feat: 补充数据库脚本和配置

This commit is contained in:
贺爱泽
2025-12-01 18:16:49 +08:00
parent 84ac31158c
commit 15fc000cfc
37 changed files with 42829 additions and 448 deletions

View File

@@ -1,62 +1,57 @@
# TODO Roadmap
# TODO Roadmap
说明本清单覆盖当前阶段的骨架搭建与核心基础能力不含部署与CI/CD留到项目跑通后再做
> 当前列表为原 11 号文档中的待办事项,已迁移到此处并统一以复选框形式标记。若无特殊说明,均尚未完成
## A. 基础骨架与规范
- [x] 统一返回结果/异常处理中间件Shared.Web
- [x] 模型验证、验证失败统一输出Shared.Web
- [x] 统一日志Serilog与请求日志/TraceIdShared.Web
- [x] API 版本化与分组AdminApi、MiniApi、UserApi
- [x] Swagger 定制(鉴权按钮、分组说明、示例)
- [x] 安全中间件Security Headers、CORS 策略(按端区分)
## 1. 配置与基础设施(高优)
- [x] Development/Production 数据库连接与 Secret 落地Staging 暂不需要)。
- [x] Redis 服务部署完毕并记录配置。
- [x] RabbitMQ 服务部署完毕并记录配置。
- [x] COS 密钥配置补录完毕。
- [ ] OSS 密钥配置补录完毕(待采购)。
- [ ] SMS 平台密钥配置补录完毕(待采购)。
- [x] WeChat Mini 程序密钥配置补录完毕AppIDwx30f91e6afe79f405AppSecret64324a7f604245301066ba7c3add488e已同步到 admin/mini 配置并登记更新人)。
- [x] PostgreSQL 基础实例部署完毕并记录配置。
- [x] Postgres/Redis 接入文档 + IaC/脚本补齐(见 Document/infra/postgres_redis.md 与 deploy/postgres|redis
- [x] RabbitMQ/Redis/Hangfire storage scripts available (see deploy/postgres and deploy/redis).
- [ ] admin/mini/user/gateway 网关域名、证书、CORS 列表整理完成。
- [ ] Hangfire Dashboard 启用并新增 Admin 角色验证/网关白名单。
## B. 认证与权限
- [x] JWT 颁发与刷新AdminApi、MiniApi
- [x] RBAC 权限模型(角色/权限/策略与特性授权AdminApi
- [x] 小程序登录(微信 code2Session并绑定用户账户MiniApi
- [x] 登录防刷限流MiniApi
## 2. 数据与迁移(高优)
- [ ] App/Identity/Dictionary/Hangfire 四个 DbContext 均生成初始 Migration 并成功 update database。
- [ ] 商户/门店/商品/订单/支付/配送等实体与仓储实现完成,提供 CRUD + 查询。
- [ ] 系统参数、默认租户、管理员账号、基础字典的种子脚本可重复执行。
## C. 多租户与参数字典
- [x] 多租户中间件:从 Header/域名解析租户Shared.Web + Tenancy
- [x] EF Core 全局查询过滤tenant_id
- [x] 参数字典模块(系统参数/业务参数CRUD 与缓存Dictionary 模块)
## 3. 稳定性与质量
- [ ] Dictionary/Identity/Storage/Sms/Messaging/Scheduler 的 xUnit+FluentAssertions 单元测试框架搭建。
- [ ] WebApplicationFactory + Testcontainers 拉起 Postgres/Redis/RabbitMQ/MinIO 的集成测试模板。
- [ ] .editorconfig、.globalconfig、Roslyn 分析器配置仓库通用规则并启用 CI 检查。
## D. 数据访问与多数据源
- [x] EF Core 10 基础上下文、实体基类、审计字段
- [x] 读写分离/多数据源配置(主写、从读)
- [x] Dapper 基础设施封装(统计/报表类查询)
## 4. 安全与合规
- [ ] RBAC 权限、租户隔离、用户/权限洞察 API 完整演示并在 Swagger 中提供示例。
- [ ] 登录/刷新流程增加 IP 校验、租户隔离、验证码/频率限制。
- [ ] 登录/权限/敏感操作日志可追溯,提供查询接口或 Kibana Saved Search。
- [ ] Secret Store/KeyVault/KMS 管理敏感配置,禁止密钥写入 Git/数据库明文。
## E. 文件与存储
- [x] 存储模块抽象腾讯云COS/七牛云/阿里云OSS
- [x] 上传接口AdminApi、MiniApi与签名直传预留
- [x] 图片/文件访问安全策略(防盗链、过期签名)
## 5. 观测与运维
- [ ] TraceId 贯通,并在 Serilog 中输出 Console/File/ELK 三种目标。
- [ ] Prometheus exporter 暴露关键指标,/health 探针与告警规则同步推送。
- [ ] PostgreSQL 全量/增量备份脚本及一次真实恢复演练报告。
## F. 短信与消息队列
- [x] 短信模块(阿里云/腾讯云 适配占位)与验证码发送
- [x] MQ 模块RabbitMQPublisher/Subscriber 抽象
- [x] 业务事件定义(订单创建/支付成功等)与事件发布入口
## 6. 业务能力补全
- [ ] 商户/门店/菜品 API 完成并在 MQ 中投递上架/支付成功事件。
- [ ] 配送对接 API 支持下单/取消/查询并完成签名验签中间件。
- [ ] 小程序端商品浏览、下单、支付、评价、图片直传等 API 可闭环跑通。
## G. 调度与定时任务
- [x] 调度模块Quartz/Hangfire 二选一,默认 Hangfire
- [x] 基础任务:订单超时取消、优惠券过期处理、日志清理
- [x] 调度面板(后续 AdminUI 对接)
## 7. 前后台 UI 对接
- [ ] Admin UI 通过 OpenAPI 生成或手写界面,接入 Hangfire Dashboard/MQ 监控只读模式。
- [ ] 小程序端完成登录、菜单浏览、下单、支付、物流轨迹、素材直传闭环。
## H. 第三方配送对接(仅第三方)
- [ ] 配送适配抽象(达达/闪送/顺丰同城等)
- [ ] 统一下单/取消/查询接口与回调验签
- [ ] AdminApi 后台运力单查询与补单
## 8. CI/CD 与发布
- [ ] CI/CD 流水线覆盖构建、发布、静态扫描、数据库迁移。
- [ ] Dev/Staging/Prod 多环境配置矩阵 + 基础设施 IaC 脚本。
- [ ] 版本与发布说明模板整理并在仓库中提供示例。
## I. 网关与横切能力
- [x] YARP 路由拆分(/api/admin、/api/mini、/api/user
- [x] 网关级限流与请求日志
- [x] 透传鉴权/租户标识与统一错误页
## J. 测试与质量
- [ ] 单元测试工程骨架xUnit + FluentAssertions
- [ ] 集成测试基座WebApplicationFactory、测试容器
- [ ] 静态分析与风格规范(.editorconfig
## K. 文档与规范落地
- [ ] 在文档中补充:仅第三方配送的接口与回调规范
- [ ] MiniApi 认证流程图(微信登录)与错误码
- [ ] 模块间调用关系图与依赖边界
## 9. 文档与知识库
- [ ] 接口文档、领域模型、关键约束使用 Markdown 或 API Portal 完整记录。
- [ ] 运行手册包含部署步骤、资源拓扑、故障排查手册。
- [ ] 安全合规模板覆盖数据分级、密钥管理、审计流程并形成可复用表格。

View File

@@ -1,49 +1,53 @@
# 下一步 TODO骨架完成后
# 里程碑待办追踪
说明当前骨架已覆盖认证、权限、多租户、存储、短信、MQ、调度、网关等基础能力。下面的清单用于进入“可运行/可上线”的补全与质量阶段,可按优先级推进。
> 按“小程序版模块规划”划分四个里程碑;每个里程碑只含对应范围的任务,便于分阶段推进。
## 1. 配置与基础设施落地(高优)
- 补充真实配置:数据库/Redis/RabbitMQ/对象存储/SMS/WeChat Mini/身份密钥并分环境管理Development/Staging/Production
- 准备基础设施PostgreSQL 主从、Redis哨兵/集群、RabbitMQ、COS/OSS、Hangfire 存储库;完善 docker-compose 与部署说明
- 网关与服务域名规划:为 admin/mini/user/gateway 配置实际域名、TLS 证书与 CORS 列表
- Hangfire Dashboard 鉴权:开启并加上 Admin 角色校验或网关白名单
---
## Phase 1当前阶段租户/商家入驻、门店与菜品、扫码堂食、基础下单支付、预购自提、第三方配送骨架
- [ ] 管理端租户 API注册、实名认证、套餐订阅/续费/升降配、审核流Swagger ≥6 个端点,含审核日志
- [ ] 商家入驻 API证照上传、合同管理、类目选择驱动待审/审核/驳回/通过状态机,文件持久在 COS
- [ ] RBAC 模板平台管理员、租户管理员、店长、店员四角色模板API 可复制并允许租户自定义扩展
- [ ] 配额与套餐TenantPackage CRUD、订阅/续费/配额校验(门店/账号/短信/配送单量),超额返回 409 并记录 TenantQuotaUsage。
- [ ] 租户运营面板:欠费/到期告警、账单列表、公告通知接口,支持已读状态并在 Admin UI 展示。
- [ ] 门店管理Store/StoreBusinessHour/StoreDeliveryZone/StoreHoliday CRUD 完整,含 GeoJSON 配送范围及能力开关。
- [ ] 桌码管理:批量生成桌码、绑定区域/容量、导出二维码 ZIPPOST /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 条自动化用例(单元 + 集成),覆盖租户→商户→下单链路。
## 2. 数据与迁移(高优)
- 建立 EF Core Migration 基线并生成数据库App/Identity/Dictionary/Hangfire
- 设计并落地核心业务表(商户/门店/商品/订单/支付/配送等),补齐 Domain 与 Infrastructure 仓储
- 数据初始化/种子:系统参数、默认租户、管理员、基础字典
---
## Phase 2下一阶段拼单、优惠券与基础营销、会员积分/会员日、客服聊天、同城自配送调度、搜索
- [ ] 拼单引擎GroupOrder/Participant CRUD、发起/加入/成团条件、自动解散与退款、团内消息与提醒
- [ ] 优惠券与基础营销:模板管理、领券、核销、库存/有效期/叠加规则,基础抽奖/秒杀/满减活动
- [ ] 会员与积分:会员档案、等级/成长值、会员日通知;积分获取/消耗、有效期、黑名单。
- [ ] 客服聊天:实时会话、机器人/人工切换、排队/转接、消息模板、敏感词审查、工单流转与评价。
- [ ] 同城自配送调度:骑手智能指派、路线估时、无接触配送、费用补贴策略、调度看板。
- [ ] 搜索:门店/菜品/活动/优惠券搜索,过滤/排序、热门/历史记录、联想与纠错。
## 3. 质量与测试(高优)
- 单元测试骨架xUnit + FluentAssertionsDictionary、Identity、Storage、Sms、Messaging、Scheduler
- 集成测试基座WebApplicationFactory + TestcontainersPostgres/Redis/RabbitMQ/MinIO 可选)
- 静态分析:添加 .editorconfig/.globalconfig启用可空警告、风格规则接入 Roslyn 分析器
---
## Phase 3分销返利、签到打卡、预约预订、地图导航、社区、高阶营销、风控与补偿
- [ ] 分销返利AffiliatePartner/Order/Payout 管理,佣金阶梯、结算周期、税务信息、违规处理
- [ ] 签到打卡CheckInCampaign/Record、连签奖励、补签、积分/券/成长值奖励、反作弊机制
- [ ] 预约预订:档期/资源占用、预约下单/支付、提醒/改期/取消、到店核销与履约记录。
- [ ] 地图导航扩展:附近门店/推荐、距离/路线规划、跳转原生导航、导航请求埋点。
- [ ] 社区:动态发布、评论、点赞、话题/标签、图片/视频审核、举报与风控,店铺口碑展示。
- [ ] 高阶营销:秒杀/抽奖/裂变、裂变海报、爆款推荐位、多渠道投放分析。
- [ ] 风控与审计:黑名单、频率限制、异常行为监控、审计日志、补偿与告警体系。
## 4. 安全与合规
- 完善鉴权:网关透传与后端校验的租户/用户/权限Swagger 鉴权示例。
- 输入校验与防刷:全局限流策略(按 IP/租户),登录与验证码防刷策略参数化
- 日志与审计:敏感字段脱敏,登录/权限/管理操作审计日志模型与落库
- 配置机密:使用 Secret Store/环境变量/KMS 管理密钥,禁止明文提交
## 5. 可观测性与运维
- 日志链路:统一 TraceId 透传(网关→服务),配置 Serilog 输出Console/File/ELK与留存策略。
- 指标/监控Prometheus exporter、健康检查探针/health、告警规则草案。
- 备份恢复PostgreSQL 全量/增量备份脚本,恢复演练记录。
## 6. 业务功能补全
- 订单/商品/商户等领域建模与应用服务接口实现,结合 MQ 事件发布(订单创建、支付成功等)。
- 配送对接抽象实现(达达/闪送/顺丰同城)占位,提供下单/取消/查询接口与回调验签。
- 小程序端接口补齐:商品浏览、下单、支付、评价、上传图片直传联调。
## 7. 前台/后台 UI 对接
- Admin UI接入 Swagger 导出的 OpenAPI生成或手写管理端界面接入 Hangfire Dashboard/MQ 监控只读访问。
- MiniApp小程序登录流程与错误码文档完善联调上传、下单、支付链路。
## 8. CI/CD 与发布
- 建立流水线:构建/测试/扫描SAST、镜像推送、数据库迁移步骤。
- 多环境部署策略Dev/Staging/Prod 配置隔离,蓝绿或滚动发布方案草拟。
- 版本与变更管理:约定版本号/发布说明模板。
## 9. 文档补全
- 更新接口文档(新增业务 API、错误码、回调规范、模块依赖关系图。
- 运维手册:启动参数、环境变量列表、端口/域名映射、常见故障排查。
- 安全与合规清单:数据分类分级、审计、留存周期。
---
## Phase 4性能优化、缓存、运营大盘、测试与文档、上线与监控
- [ ] 性能与缓存:热点接口缓存、慢查询治理、批处理优化、异步化改造
- [ ] 可靠性:幂等与重试策略、任务调度补偿、链路追踪、告警联动
- [ ] 运营大盘:交易/营销/履约/用户维度的细分报表、GMV/成本/毛利分析
- [ ] 文档与测试:完整测试矩阵、性能测试报告、上线手册、回滚方案。
- [ ] 监控与运维:上线发布流程、灰度/回滚策略、系统稳定性指标、24x7 监控与告警。

View File

@@ -0,0 +1,89 @@
# 设计时 DbContext 配置指引
> 目的:在执行 `dotnet ef` 命令时无需硬编码数据库连接,可根据 appsettings 与环境变量自动加载。本文覆盖环境变量设置、配置目录指定等细节。
## 一、设计时工厂读取逻辑概述
设计时工厂(`DesignTimeDbContextFactoryBase<T>`)按下面顺序解析连接串:
1. 若设置了 `TAKEOUTSAAS_APP_CONNECTION` / `TAKEOUTSAAS_IDENTITY_CONNECTION` / `TAKEOUTSAAS_DICTIONARY_CONNECTION` 等环境变量,则优先使用。
2. 否则查找配置文件:
- 从当前目录开始向上找到含 `TakeoutSaaS.sln` 的仓库根。
- 依次检查 `src/Api/TakeoutSaaS.AdminApi``src/Api/TakeoutSaaS.UserApi``src/Api/TakeoutSaaS.MiniApi` 等目录,如果存在 `appsettings.json``appsettings.{Environment}.json` 则加载。
- 若未找到,可通过环境变量 `TAKEOUTSAAS_APPSETTINGS_DIR` 指定包含 appsettings 文件的目录。
配置结构示例(出现在 AdminApi/MiniApi/UserApi 的 appsettings
```json
"Database": {
"DataSources": {
"AppDatabase": {
"Write": "Host=120.53...;Database=takeout_app_db;Username=...;Password=...",
"Reads": [
"Host=120.53...;Database=takeout_app_db;Username=...;Password=..."
]
},
"IdentityDatabase": {
"Write": "...",
"Reads": [ "..." ]
},
"DictionaryDatabase": {
"Write": "...",
"Reads": [ "..." ]
}
}
}
```
设计时工厂会根据数据源名称(`DatabaseConstants.AppDataSource` 等)读取 `Write` 连接串,实现与运行时一致。
## 二、环境变量配置
### 1. Windows PowerShell
```powershell
# 指向包含 appsettings.json 的目录
$env:TAKEOUTSAAS_APPSETTINGS_DIR = \"D:\\HAZCode\\TakeOut\\src\\Api\\TakeoutSaaS.AdminApi\"
#(可选)覆盖 AppDatabase 连接串
$env:TAKEOUTSAAS_APP_CONNECTION = \"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=***\"
#(可选)覆盖 IdentityDatabase 连接串
$env:TAKEOUTSAAS_IDENTITY_CONNECTION = \"Host=...;Database=takeout_identity_db;Username=...;Password=...\"
#<23><><EFBFBD><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD> DictionaryDatabase <20><><EFBFBD>Ӵ<EFBFBD>
$env:TAKEOUTSAAS_DICTIONARY_CONNECTION = "Host=...;Database=takeout_dictionary_db;Username=...;Password=..."
```
### 2. Linux / macOS
```bash
export TAKEOUTSAAS_APPSETTINGS_DIR=/home/user/TakeOut/src/Api/TakeoutSaaS.AdminApi
export TAKEOUTSAAS_APP_CONNECTION=\"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=***\"
export TAKEOUTSAAS_IDENTITY_CONNECTION=\"Host=...;Database=takeout_identity_db;Username=...;Password=...\"
export TAKEOUTSAAS_DICTIONARY_CONNECTION="Host=...;Database=takeout_dictionary_db;Username=...;Password=..."
```
> 注意:若设置了 `TAKEOUTSAAS_APP_CONNECTION`,则无需在 appsettings 中提供 `Write` 连接串,反之亦然。不要将明文密码写入代码仓库,建议使用 Secret Manager 或部署环境的安全存储。
## 三、执行脚本示例
完成上述环境变量配置后即可执行:
```powershell
# TakeoutAppDbContext业务库
dotnet tool run dotnet-ef database update `
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
--startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
--context TakeoutSaaS.Infrastructure.App.Persistence.TakeoutAppDbContext
# IdentityDbContext身份库
dotnet tool run dotnet-ef database update `
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
--startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
--context TakeoutSaaS.Infrastructure.Identity.Persistence.IdentityDbContext
# DictionaryDbContext字典库
dotnet tool run dotnet-ef database update `
--project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
--startup-project src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj `
--context TakeoutSaaS.Infrastructure.Dictionary.Persistence.DictionaryDbContext
```
若需迁移 Identity/Dictionary 等上下文,替换 `--context` 参数为对应类型即可。
## 四、常见问题
1. **未找到 appsettings**:确保 `TAKEOUTSAAS_APPSETTINGS_DIR` 指向存在 `appsettings.json` 的目录,或将命令在 API 项目目录中执行。
2. **密码错误**:确认远程 PostgreSQL 用户/密码是否与 appsettings 或环境变量一致,避免在 CLI 中使用默认的账号。
3. **多环境配置**`ASPNETCORE_ENVIRONMENT` 变量可控制加载 `appsettings.{Environment}.json`;默认是 Development。

View File

@@ -0,0 +1,101 @@
# PostgreSQL 与 Redis 接入手册
> 本文档补齐 `Document/10_TODO.md` 中“Postgres/Redis 接入文档与 IaC/脚本”的要求,统一描述连接信息、账号权限、运维流程,以及可复用的部署脚本位置。
## 1. 运行环境总览
| 组件 | 地址/端口 | 主要数据库/实例 | 说明 |
| --- | --- | --- | --- |
| PostgreSQL | `120.53.222.17:5432` | `takeout_app_db``takeout_identity_db``takeout_dictionary_db``takeout_hangfire_db` | 线上实例,所有业务上下文共用。 |
| Redis | `49.232.6.45:6379` | 单节点 | 业务缓存/登录限流/刷新令牌存储。 |
> 注意:所有业务账号都只具备既有库的读写权限,无 `CREATEDB`。若需新库,需使用平台管理员账号(`postgres`)或联系 DBA。
## 2. 账号与库映射
| 数据库 | 角色 | 密码 | 用途 |
| --- | --- | --- | --- |
| `takeout_app_db` | `app_user` | `AppUser112233` | 业务域 (`TakeoutAppDbContext`) |
| `takeout_identity_db` | `identity_user` | `IdentityUser112233` | 身份域 (`IdentityDbContext`) |
| `takeout_dictionary_db` | `dictionary_user` | `DictionaryUser112233` | 字典域 (`DictionaryDbContext`) |
| `takeout_hangfire_db` | `hangfire_user` | `HangFire112233` | 后台调度/Hangfire |
Redis 密码:`MsuMshk112233`,见 `appsettings.*.json -> Redis`
## 3. 环境变量/配置注入
### PowerShell
```powershell
$env:TAKEOUTSAAS_APPSETTINGS_DIR = "D:\HAZCode\TakeOut\src\Api\TakeoutSaaS.AdminApi"
$env:TAKEOUTSAAS_APP_CONNECTION = "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true"
$env:TAKEOUTSAAS_IDENTITY_CONNECTION = "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true"
$env:TAKEOUTSAAS_DICTIONARY_CONNECTION = "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true"
```
### Bash
```bash
export TAKEOUTSAAS_APPSETTINGS_DIR=/home/user/TakeOut/src/Api/TakeoutSaaS.AdminApi
export TAKEOUTSAAS_APP_CONNECTION="Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true"
export TAKEOUTSAAS_IDENTITY_CONNECTION="Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true"
export TAKEOUTSAAS_DICTIONARY_CONNECTION="Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true"
```
Redis 连接字符串直接写入 `appsettings.*.json` 即可,如:
```jsonc
"Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false"
```
## 4. 运维指南
### PostgreSQL
1. **只读账号验证**
```powershell
psql "host=120.53.222.17 port=5432 dbname=takeout_app_db user=app_user password=AppUser112233"
```
2. **备份**
```bash
pg_dump -h 120.53.222.17 -p 5432 -U postgres -F c -d takeout_app_db -f backup/takeout_app_db_$(date +%Y%m%d).dump
pg_dumpall -h 120.53.222.17 -p 5432 -U postgres > backup/all_$(date +%Y%m%d).sql
```
3. **恢复**
```bash
pg_restore -h 120.53.222.17 -p 5432 -U postgres -d takeout_app_db backup/takeout_app_db_xxx.dump
psql -h 120.53.222.17 -p 5432 -U postgres -f backup/all_yyyymmdd.sql
```
4. **账号/权限策略**
- `app_user` / `identity_user` / `dictionary_user` 拥有 `CONNECT`、`TEMP`、Schema `public` 的 CRUD 权限。
- `hangfire_user` 仅能访问 `takeout_hangfire_db`,不可访问业务库。
- 创建新表/列时,通过 EF Migration 自动添加 COMMENT。
### Redis
1. **连接验证**
```bash
redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 ping
```
2. **备份**
```bash
redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 save # 触发 RDB
redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 bgsave # 后台
```
RDB/AOF 文件在服务器 `redis.conf` 定义的目录(默认 `/var/lib/redis`)。
3. **常见运维项**
- `CONFIG GET dir` / `CONFIG GET dbfilename` 可查看持久化路径。
- `INFO memory` 监控内存;开启 `maxmemory` + `allkeys-lru` 保护。
## 5. IaC / 脚本
| 文件 | 说明 |
| --- | --- |
| `deploy/postgres/create_databases.sql` | 基于 `postgres` 管理员执行,创建四个业务库及角色、授予权限、补 COMMENT。 |
| `deploy/postgres/bootstrap.ps1` | PowerShell 包装脚本,调用 `psql` 执行上面的 SQL默认读取 `postgres` 管理员账号)。 |
| `deploy/postgres/README.md` | 介绍如何在本地/测试环境执行 bootstrap 并校验连接。 |
| `deploy/redis/docker-compose.yml` | 可复用的 Redis 部署Redis 7 + AOF便于本地或测试环境一键拉起。 |
| `deploy/redis/redis.conf` | compose/裸机均可共用的配置(`requirepass`、持久化等已写好)。 |
| `deploy/redis/README.md` | 说明如何使用 compose 或将 `redis.conf` 部署到现有实例。 |
> 线上目前为裸机安装(非容器),如需创建新环境/快速恢复,可直接运行上述脚本达到同样配置;即使在现有机器上,也可把 SQL/配置当作“最终规范”确保环境一致性。

47
deploy/postgres/README.md Normal file
View File

@@ -0,0 +1,47 @@
# PostgreSQL 部署脚本
本目录提供在测试/预发布环境快速拉起 PostgreSQL 的脚本,复用线上同名数据库与账号,方便迁移/恢复。
## 目录结构
- `create_databases.sql`:创建四个业务库与对应角色(可多次执行,存在则跳过)。
- `bootstrap.ps1`PowerShell 包装脚本,调用 `psql` 执行 SQL。
## 前置条件
1. 已安装 PostgreSQL 12+,并能以管理员身份访问(默认使用 `postgres`)。
2. 本地已配置 `psql` 可执行命令。
## 使用方法
```powershell
cd deploy/postgres
.\bootstrap.ps1 `
-Host 120.53.222.17 `
-Port 5432 `
-AdminUser postgres `
-AdminPassword "超级管理员密码"
```
脚本会:
1. 创建/更新以下角色与库:
- `app_user` / `takeout_app_db`
- `identity_user` / `takeout_identity_db`
- `dictionary_user` / `takeout_dictionary_db`
- `hangfire_user` / `takeout_hangfire_db`
2. 为库设置 COMMENT授予 Schema `public` 的 CRUD 权限。
3. 输出执行日志,失败时终止。
## 自定义
- 如需修改密码或新增库,编辑 `create_databases.sql` 后重新运行脚本。
- 若在本地拉起测试库,可把 `Host` 指向 `localhost`,其余参数保持一致。
## 常见问题
| 问题 | 处理方式 |
| --- | --- |
| `psql : command not found` | 确认 PostgreSQL bin 目录已加入 PATH。 |
| `permission denied to create database` | 改用具有 `CREATEDB` 权限的管理员执行脚本。 |
| 需要删除库 | 先 `DROP DATABASE xxx`,再运行脚本重新创建。 |

View File

@@ -0,0 +1,37 @@
param(
[string]$Host = "120.53.222.17",
[int]$Port = 5432,
[string]$AdminUser = "postgres",
[string]$AdminPassword = ""
)
if (-not (Get-Command psql -ErrorAction SilentlyContinue)) {
throw "psql command not found. Add PostgreSQL bin directory to PATH."
}
if ([string]::IsNullOrWhiteSpace($AdminPassword)) {
Write-Warning "AdminPassword not provided. You will be prompted by psql."
}
$sqlPath = Join-Path $PSScriptRoot "create_databases.sql"
if (-not (Test-Path $sqlPath)) {
throw "Cannot find create_databases.sql under $PSScriptRoot."
}
$env:PGPASSWORD = $AdminPassword
$arguments = @(
"-h", $Host,
"-p", $Port,
"-U", $AdminUser,
"-f", $sqlPath
)
Write-Host "Executing create_databases.sql on $Host:$Port as $AdminUser ..."
& psql @arguments
if ($LASTEXITCODE -ne 0) {
throw "psql returned non-zero exit code ($LASTEXITCODE)."
}
Write-Host "PostgreSQL databases and roles ensured successfully."

View File

@@ -0,0 +1,83 @@
-- Reusable provisioning script for Takeout SaaS PostgreSQL databases.
-- Execute with a superuser (e.g. postgres). Safe to re-run.
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_user') THEN
CREATE ROLE app_user LOGIN PASSWORD 'AppUser112233';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'identity_user') THEN
CREATE ROLE identity_user LOGIN PASSWORD 'IdentityUser112233';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'dictionary_user') THEN
CREATE ROLE dictionary_user LOGIN PASSWORD 'DictionaryUser112233';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hangfire_user') THEN
CREATE ROLE hangfire_user LOGIN PASSWORD 'HangFire112233';
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_app_db') THEN
CREATE DATABASE takeout_app_db OWNER app_user ENCODING 'UTF8';
END IF;
END $$;
COMMENT ON DATABASE takeout_app_db IS 'Takeout SaaS 业务域数据库';
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_identity_db') THEN
CREATE DATABASE takeout_identity_db OWNER identity_user ENCODING 'UTF8';
END IF;
END $$;
COMMENT ON DATABASE takeout_identity_db IS 'Takeout SaaS 身份域数据库';
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_dictionary_db') THEN
CREATE DATABASE takeout_dictionary_db OWNER dictionary_user ENCODING 'UTF8';
END IF;
END $$;
COMMENT ON DATABASE takeout_dictionary_db IS 'Takeout SaaS 字典域数据库';
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = 'takeout_hangfire_db') THEN
CREATE DATABASE takeout_hangfire_db OWNER hangfire_user ENCODING 'UTF8';
END IF;
END $$;
COMMENT ON DATABASE takeout_hangfire_db IS 'Takeout SaaS 调度/Hangfire 数据库';
-- Ensure privileges and default schema permissions
\connect takeout_app_db
GRANT CONNECT, TEMP ON DATABASE takeout_app_db TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO app_user;
\connect takeout_identity_db
GRANT CONNECT, TEMP ON DATABASE takeout_identity_db TO identity_user;
GRANT USAGE ON SCHEMA public TO identity_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO identity_user;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO identity_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO identity_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO identity_user;
\connect takeout_dictionary_db
GRANT CONNECT, TEMP ON DATABASE takeout_dictionary_db TO dictionary_user;
GRANT USAGE ON SCHEMA public TO dictionary_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO dictionary_user;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO dictionary_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO dictionary_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO dictionary_user;
\connect takeout_hangfire_db
GRANT CONNECT, TEMP ON DATABASE takeout_hangfire_db TO hangfire_user;
GRANT USAGE ON SCHEMA public TO hangfire_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO hangfire_user;
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO hangfire_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO hangfire_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO hangfire_user;

34
deploy/redis/README.md Normal file
View File

@@ -0,0 +1,34 @@
# Redis 部署脚本
本目录提供可复用的 Redis 配置,既可在本地通过 Docker Compose 启动,也可将 `redis.conf` 拷贝到现有服务器,确保与线上一致。
## 1. 部署步骤 (裸机)\n\n1. 将 \\
edis.conf\\ 拷贝到服务器(例如 /etc/redis/redis.conf。\n2. 根据需要修改数据目录(\\dir\\)和绑定地址。\n3. 使用系统服务或 \\
edis-server redis.conf\\ 启动。\n4. 确认开放端口 6379保证通过 \\
edis-cli -h <host> -a <pwd> ping\\ 可访问。\n\n## 2. 配置说明\n\n- \\
equirepass\\ 已设置为 MsuMshk112233。\n- 启用 appendonlyAOF并每秒 fsync。\n- \\maxmemory-policy\\ 为 allkeys-lru适合缓存场景。\n- \\protected-mode no\\ 允许远程连接,需结合安全组或防火墙限制来源 IP。\n\n## 3. 常用命令使用 `redis.conf`
1.`redis.conf` 拷贝到服务器 `/etc/redis/redis.conf`(或自定义目录)。
2. 修改 `dir` 指向实际数据目录。
3. 使用系统服务或 `redis-server redis.conf` 启动。
关键配置已包含:
- `requirepass`(密码)
- `protected-mode no`(允许远程连接)
- `appendonly yes` + `appendfsync everysec`
- `maxmemory-policy allkeys-lru`
## 3. 常用命令
在应用或 CLI 中使用:
```bash
redis-cli -h 49.232.6.45 -p 6379 -a MsuMshk112233 ping
```
`appsettings.*.json` 的格式:`"Redis": "49.232.6.45:6379,password=MsuMshk112233,abortConnect=false"`
## 4. 备份
- RDB 文件:`dump.rdb`

25
deploy/redis/redis.conf Normal file
View File

@@ -0,0 +1,25 @@
bind 0.0.0.0
port 6379
protected-mode no
requirepass MsuMshk112233
timeout 0
tcp-keepalive 300
daemonize no
loglevel notice
databases 16
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
dir /data
maxmemory-policy allkeys-lru

View File

@@ -2,18 +2,27 @@
"Database": {
"DataSources": {
"AppDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"IdentityDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"DictionaryDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
@@ -134,7 +143,7 @@
"PrefetchCount": 20
},
"Scheduler": {
"ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_saas_hangfire;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_hangfire_db;Username=hangfire_user;Password=HangFire112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"WorkerCount": 5,
"DashboardEnabled": false,
"DashboardPath": "/hangfire"

View File

@@ -2,18 +2,27 @@
"Database": {
"DataSources": {
"AppDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"IdentityDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"DictionaryDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
@@ -134,7 +143,7 @@
"PrefetchCount": 20
},
"Scheduler": {
"ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_saas_hangfire;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"ConnectionString": "Host=120.53.222.17;Port=5432;Database=takeout_hangfire_db;Username=hangfire_user;Password=HangFire112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"WorkerCount": 5,
"DashboardEnabled": false,
"DashboardPath": "/hangfire"

View File

@@ -2,18 +2,27 @@
"Database": {
"DataSources": {
"AppDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"IdentityDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"DictionaryDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,

View File

@@ -2,18 +2,27 @@
"Database": {
"DataSources": {
"AppDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"IdentityDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"DictionaryDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,

View File

@@ -2,18 +2,27 @@
"Database": {
"DataSources": {
"AppDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"IdentityDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"DictionaryDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,

View File

@@ -2,18 +2,27 @@
"Database": {
"DataSources": {
"AppDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_app;Username=app_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_app_db;Username=app_user;Password=AppUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"IdentityDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_saas_identity;Username=identity_user;Password=MsuMshk112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
"Host=120.53.222.17;Port=5432;Database=takeout_identity_db;Username=identity_user;Password=IdentityUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,
"MaxRetryDelaySeconds": 5
},
"DictionaryDatabase": {
"Write": "Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50",
"Reads": [
"Host=120.53.222.17;Port=5432;Database=takeout_dictionary_db;Username=dictionary_user;Password=DictionaryUser112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50"
],
"CommandTimeoutSeconds": 30,
"MaxRetryCount": 3,

View File

@@ -14,4 +14,9 @@ public static class DatabaseConstants
/// 身份认证库IdentityDatabase
/// </summary>
public const string IdentityDataSource = "IdentityDatabase";
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD>ֵ<EFBFBD>⣨DictionaryDatabase<73><65><EFBFBD><EFBFBD>
/// </summary>
public const string DictionaryDataSource = "DictionaryDatabase";
}

View File

@@ -3,6 +3,8 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1591</NoWarn>
</PropertyGroup>
</Project>

View File

@@ -3,6 +3,8 @@
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />

View File

@@ -1,7 +1,17 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using TakeoutSaaS.Domain.Analytics.Entities;
using TakeoutSaaS.Domain.Coupons.Entities;
using TakeoutSaaS.Domain.CustomerService.Entities;
using TakeoutSaaS.Domain.Deliveries.Entities;
using TakeoutSaaS.Domain.Distribution.Entities;
using TakeoutSaaS.Domain.Engagement.Entities;
using TakeoutSaaS.Domain.GroupBuying.Entities;
using TakeoutSaaS.Domain.Inventory.Entities;
using TakeoutSaaS.Domain.Membership.Entities;
using TakeoutSaaS.Domain.Merchants.Entities;
using TakeoutSaaS.Domain.Navigation.Entities;
using TakeoutSaaS.Domain.Ordering.Entities;
using TakeoutSaaS.Domain.Orders.Entities;
using TakeoutSaaS.Domain.Payments.Entities;
using TakeoutSaaS.Domain.Products.Entities;
@@ -25,16 +35,91 @@ public sealed class TakeoutAppDbContext(
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor)
{
public DbSet<Tenant> Tenants => Set<Tenant>();
public DbSet<TenantPackage> TenantPackages => Set<TenantPackage>();
public DbSet<TenantSubscription> TenantSubscriptions => Set<TenantSubscription>();
public DbSet<TenantQuotaUsage> TenantQuotaUsages => Set<TenantQuotaUsage>();
public DbSet<TenantBillingStatement> TenantBillingStatements => Set<TenantBillingStatement>();
public DbSet<TenantNotification> TenantNotifications => Set<TenantNotification>();
public DbSet<Merchant> Merchants => Set<Merchant>();
public DbSet<MerchantDocument> MerchantDocuments => Set<MerchantDocument>();
public DbSet<MerchantContract> MerchantContracts => Set<MerchantContract>();
public DbSet<MerchantStaff> MerchantStaff => Set<MerchantStaff>();
public DbSet<Store> Stores => Set<Store>();
public DbSet<StoreBusinessHour> StoreBusinessHours => Set<StoreBusinessHour>();
public DbSet<StoreHoliday> StoreHolidays => Set<StoreHoliday>();
public DbSet<StoreDeliveryZone> StoreDeliveryZones => Set<StoreDeliveryZone>();
public DbSet<StoreTableArea> StoreTableAreas => Set<StoreTableArea>();
public DbSet<StoreTable> StoreTables => Set<StoreTable>();
public DbSet<StoreEmployeeShift> StoreEmployeeShifts => Set<StoreEmployeeShift>();
public DbSet<ProductCategory> ProductCategories => Set<ProductCategory>();
public DbSet<Product> Products => Set<Product>();
public DbSet<ProductAttributeGroup> ProductAttributeGroups => Set<ProductAttributeGroup>();
public DbSet<ProductAttributeOption> ProductAttributeOptions => Set<ProductAttributeOption>();
public DbSet<ProductSku> ProductSkus => Set<ProductSku>();
public DbSet<ProductAddonGroup> ProductAddonGroups => Set<ProductAddonGroup>();
public DbSet<ProductAddonOption> ProductAddonOptions => Set<ProductAddonOption>();
public DbSet<ProductPricingRule> ProductPricingRules => Set<ProductPricingRule>();
public DbSet<ProductMediaAsset> ProductMediaAssets => Set<ProductMediaAsset>();
public DbSet<InventoryItem> InventoryItems => Set<InventoryItem>();
public DbSet<InventoryAdjustment> InventoryAdjustments => Set<InventoryAdjustment>();
public DbSet<InventoryBatch> InventoryBatches => Set<InventoryBatch>();
public DbSet<ShoppingCart> ShoppingCarts => Set<ShoppingCart>();
public DbSet<CartItem> CartItems => Set<CartItem>();
public DbSet<CartItemAddon> CartItemAddons => Set<CartItemAddon>();
public DbSet<CheckoutSession> CheckoutSessions => Set<CheckoutSession>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
public DbSet<OrderStatusHistory> OrderStatusHistories => Set<OrderStatusHistory>();
public DbSet<RefundRequest> RefundRequests => Set<RefundRequest>();
public DbSet<PaymentRecord> PaymentRecords => Set<PaymentRecord>();
public DbSet<PaymentRefundRecord> PaymentRefundRecords => Set<PaymentRefundRecord>();
public DbSet<Reservation> Reservations => Set<Reservation>();
public DbSet<QueueTicket> QueueTickets => Set<QueueTicket>();
public DbSet<DeliveryOrder> DeliveryOrders => Set<DeliveryOrder>();
public DbSet<DeliveryEvent> DeliveryEvents => Set<DeliveryEvent>();
public DbSet<GroupOrder> GroupOrders => Set<GroupOrder>();
public DbSet<GroupParticipant> GroupParticipants => Set<GroupParticipant>();
public DbSet<CouponTemplate> CouponTemplates => Set<CouponTemplate>();
public DbSet<Coupon> Coupons => Set<Coupon>();
public DbSet<PromotionCampaign> PromotionCampaigns => Set<PromotionCampaign>();
public DbSet<MemberProfile> MemberProfiles => Set<MemberProfile>();
public DbSet<MemberTier> MemberTiers => Set<MemberTier>();
public DbSet<MemberPointLedger> MemberPointLedgers => Set<MemberPointLedger>();
public DbSet<MemberGrowthLog> MemberGrowthLogs => Set<MemberGrowthLog>();
public DbSet<ChatSession> ChatSessions => Set<ChatSession>();
public DbSet<ChatMessage> ChatMessages => Set<ChatMessage>();
public DbSet<SupportTicket> SupportTickets => Set<SupportTicket>();
public DbSet<TicketComment> TicketComments => Set<TicketComment>();
public DbSet<AffiliatePartner> AffiliatePartners => Set<AffiliatePartner>();
public DbSet<AffiliateOrder> AffiliateOrders => Set<AffiliateOrder>();
public DbSet<AffiliatePayout> AffiliatePayouts => Set<AffiliatePayout>();
public DbSet<CheckInCampaign> CheckInCampaigns => Set<CheckInCampaign>();
public DbSet<CheckInRecord> CheckInRecords => Set<CheckInRecord>();
public DbSet<CommunityPost> CommunityPosts => Set<CommunityPost>();
public DbSet<CommunityComment> CommunityComments => Set<CommunityComment>();
public DbSet<CommunityReaction> CommunityReactions => Set<CommunityReaction>();
public DbSet<MapLocation> MapLocations => Set<MapLocation>();
public DbSet<NavigationRequest> NavigationRequests => Set<NavigationRequest>();
public DbSet<MetricDefinition> MetricDefinitions => Set<MetricDefinition>();
public DbSet<MetricSnapshot> MetricSnapshots => Set<MetricSnapshot>();
public DbSet<MetricAlertRule> MetricAlertRules => Set<MetricAlertRule>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -43,14 +128,72 @@ public sealed class TakeoutAppDbContext(
ConfigureTenant(modelBuilder.Entity<Tenant>());
ConfigureMerchant(modelBuilder.Entity<Merchant>());
ConfigureStore(modelBuilder.Entity<Store>());
ConfigureTenantPackage(modelBuilder.Entity<TenantPackage>());
ConfigureTenantSubscription(modelBuilder.Entity<TenantSubscription>());
ConfigureTenantQuotaUsage(modelBuilder.Entity<TenantQuotaUsage>());
ConfigureTenantBilling(modelBuilder.Entity<TenantBillingStatement>());
ConfigureTenantNotification(modelBuilder.Entity<TenantNotification>());
ConfigureMerchantDocument(modelBuilder.Entity<MerchantDocument>());
ConfigureMerchantContract(modelBuilder.Entity<MerchantContract>());
ConfigureMerchantStaff(modelBuilder.Entity<MerchantStaff>());
ConfigureStoreBusinessHour(modelBuilder.Entity<StoreBusinessHour>());
ConfigureStoreHoliday(modelBuilder.Entity<StoreHoliday>());
ConfigureStoreDeliveryZone(modelBuilder.Entity<StoreDeliveryZone>());
ConfigureStoreTableArea(modelBuilder.Entity<StoreTableArea>());
ConfigureStoreTable(modelBuilder.Entity<StoreTable>());
ConfigureStoreEmployeeShift(modelBuilder.Entity<StoreEmployeeShift>());
ConfigureProductCategory(modelBuilder.Entity<ProductCategory>());
ConfigureProduct(modelBuilder.Entity<Product>());
ConfigureProductAttributeGroup(modelBuilder.Entity<ProductAttributeGroup>());
ConfigureProductAttributeOption(modelBuilder.Entity<ProductAttributeOption>());
ConfigureProductSku(modelBuilder.Entity<ProductSku>());
ConfigureProductAddonGroup(modelBuilder.Entity<ProductAddonGroup>());
ConfigureProductAddonOption(modelBuilder.Entity<ProductAddonOption>());
ConfigureProductPricingRule(modelBuilder.Entity<ProductPricingRule>());
ConfigureProductMediaAsset(modelBuilder.Entity<ProductMediaAsset>());
ConfigureInventoryItem(modelBuilder.Entity<InventoryItem>());
ConfigureInventoryAdjustment(modelBuilder.Entity<InventoryAdjustment>());
ConfigureInventoryBatch(modelBuilder.Entity<InventoryBatch>());
ConfigureShoppingCart(modelBuilder.Entity<ShoppingCart>());
ConfigureCartItem(modelBuilder.Entity<CartItem>());
ConfigureCartItemAddon(modelBuilder.Entity<CartItemAddon>());
ConfigureCheckoutSession(modelBuilder.Entity<CheckoutSession>());
ConfigureOrder(modelBuilder.Entity<Order>());
ConfigureOrderItem(modelBuilder.Entity<OrderItem>());
ConfigureOrderStatusHistory(modelBuilder.Entity<OrderStatusHistory>());
ConfigureRefundRequest(modelBuilder.Entity<RefundRequest>());
ConfigurePaymentRecord(modelBuilder.Entity<PaymentRecord>());
ConfigurePaymentRefundRecord(modelBuilder.Entity<PaymentRefundRecord>());
ConfigureReservation(modelBuilder.Entity<Reservation>());
ConfigureQueueTicket(modelBuilder.Entity<QueueTicket>());
ConfigureDelivery(modelBuilder.Entity<DeliveryOrder>());
ConfigureDeliveryEvent(modelBuilder.Entity<DeliveryEvent>());
ConfigureGroupOrder(modelBuilder.Entity<GroupOrder>());
ConfigureGroupParticipant(modelBuilder.Entity<GroupParticipant>());
ConfigureCouponTemplate(modelBuilder.Entity<CouponTemplate>());
ConfigureCoupon(modelBuilder.Entity<Coupon>());
ConfigurePromotionCampaign(modelBuilder.Entity<PromotionCampaign>());
ConfigureMemberProfile(modelBuilder.Entity<MemberProfile>());
ConfigureMemberTier(modelBuilder.Entity<MemberTier>());
ConfigureMemberPointLedger(modelBuilder.Entity<MemberPointLedger>());
ConfigureMemberGrowthLog(modelBuilder.Entity<MemberGrowthLog>());
ConfigureChatSession(modelBuilder.Entity<ChatSession>());
ConfigureChatMessage(modelBuilder.Entity<ChatMessage>());
ConfigureSupportTicket(modelBuilder.Entity<SupportTicket>());
ConfigureTicketComment(modelBuilder.Entity<TicketComment>());
ConfigureAffiliatePartner(modelBuilder.Entity<AffiliatePartner>());
ConfigureAffiliateOrder(modelBuilder.Entity<AffiliateOrder>());
ConfigureAffiliatePayout(modelBuilder.Entity<AffiliatePayout>());
ConfigureCheckInCampaign(modelBuilder.Entity<CheckInCampaign>());
ConfigureCheckInRecord(modelBuilder.Entity<CheckInRecord>());
ConfigureCommunityPost(modelBuilder.Entity<CommunityPost>());
ConfigureCommunityComment(modelBuilder.Entity<CommunityComment>());
ConfigureCommunityReaction(modelBuilder.Entity<CommunityReaction>());
ConfigureMapLocation(modelBuilder.Entity<MapLocation>());
ConfigureNavigationRequest(modelBuilder.Entity<NavigationRequest>());
ConfigureMetricDefinition(modelBuilder.Entity<MetricDefinition>());
ConfigureMetricSnapshot(modelBuilder.Entity<MetricSnapshot>());
ConfigureMetricAlertRule(modelBuilder.Entity<MetricAlertRule>());
ApplyTenantQueryFilters(modelBuilder);
}
@@ -218,4 +361,632 @@ public sealed class TakeoutAppDbContext(
builder.Property(x => x.FailureReason).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.OrderId }).IsUnique();
}
private static void ConfigureTenantPackage(EntityTypeBuilder<TenantPackage> builder)
{
builder.ToTable("tenant_packages");
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
builder.Property(x => x.Description).HasMaxLength(512);
builder.Property(x => x.FeaturePoliciesJson).HasColumnType("text");
}
private static void ConfigureTenantSubscription(EntityTypeBuilder<TenantSubscription> builder)
{
builder.ToTable("tenant_subscriptions");
builder.HasKey(x => x.Id);
builder.Property(x => x.TenantPackageId).IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.TenantPackageId });
}
private static void ConfigureTenantQuotaUsage(EntityTypeBuilder<TenantQuotaUsage> builder)
{
builder.ToTable("tenant_quota_usages");
builder.HasKey(x => x.Id);
builder.Property(x => x.QuotaType).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.QuotaType }).IsUnique();
}
private static void ConfigureTenantBilling(EntityTypeBuilder<TenantBillingStatement> builder)
{
builder.ToTable("tenant_billing_statements");
builder.HasKey(x => x.Id);
builder.Property(x => x.StatementNo).HasMaxLength(64).IsRequired();
builder.Property(x => x.AmountDue).HasPrecision(18, 2);
builder.Property(x => x.AmountPaid).HasPrecision(18, 2);
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.LineItemsJson).HasColumnType("text");
builder.HasIndex(x => new { x.TenantId, x.StatementNo }).IsUnique();
}
private static void ConfigureTenantNotification(EntityTypeBuilder<TenantNotification> builder)
{
builder.ToTable("tenant_notifications");
builder.HasKey(x => x.Id);
builder.Property(x => x.Title).HasMaxLength(128).IsRequired();
builder.Property(x => x.Message).HasMaxLength(1024).IsRequired();
builder.Property(x => x.Channel).HasConversion<int>();
builder.Property(x => x.Severity).HasConversion<int>();
builder.Property(x => x.MetadataJson).HasColumnType("text");
builder.HasIndex(x => new { x.TenantId, x.Channel, x.SentAt });
}
private static void ConfigureMerchantDocument(EntityTypeBuilder<MerchantDocument> builder)
{
builder.ToTable("merchant_documents");
builder.HasKey(x => x.Id);
builder.Property(x => x.MerchantId).IsRequired();
builder.Property(x => x.DocumentType).HasConversion<int>();
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.FileUrl).HasMaxLength(512).IsRequired();
builder.Property(x => x.DocumentNumber).HasMaxLength(64);
builder.HasIndex(x => new { x.TenantId, x.MerchantId, x.DocumentType });
}
private static void ConfigureMerchantContract(EntityTypeBuilder<MerchantContract> builder)
{
builder.ToTable("merchant_contracts");
builder.HasKey(x => x.Id);
builder.Property(x => x.MerchantId).IsRequired();
builder.Property(x => x.ContractNumber).HasMaxLength(64).IsRequired();
builder.Property(x => x.FileUrl).HasMaxLength(512).IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.TerminationReason).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.MerchantId, x.ContractNumber }).IsUnique();
}
private static void ConfigureMerchantStaff(EntityTypeBuilder<MerchantStaff> builder)
{
builder.ToTable("merchant_staff");
builder.HasKey(x => x.Id);
builder.Property(x => x.MerchantId).IsRequired();
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.Phone).HasMaxLength(32).IsRequired();
builder.Property(x => x.Email).HasMaxLength(128);
builder.Property(x => x.RoleType).HasConversion<int>();
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.PermissionsJson).HasColumnType("text");
builder.HasIndex(x => new { x.TenantId, x.MerchantId, x.Phone });
}
private static void ConfigureStoreBusinessHour(EntityTypeBuilder<StoreBusinessHour> builder)
{
builder.ToTable("store_business_hours");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.HourType).HasConversion<int>();
builder.Property(x => x.Notes).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.DayOfWeek });
}
private static void ConfigureStoreHoliday(EntityTypeBuilder<StoreHoliday> builder)
{
builder.ToTable("store_holidays");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.Reason).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Date }).IsUnique();
}
private static void ConfigureStoreDeliveryZone(EntityTypeBuilder<StoreDeliveryZone> builder)
{
builder.ToTable("store_delivery_zones");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.ZoneName).HasMaxLength(64).IsRequired();
builder.Property(x => x.PolygonGeoJson).HasColumnType("text").IsRequired();
builder.Property(x => x.MinimumOrderAmount).HasPrecision(18, 2);
builder.Property(x => x.DeliveryFee).HasPrecision(18, 2);
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ZoneName });
}
private static void ConfigureStoreTableArea(EntityTypeBuilder<StoreTableArea> builder)
{
builder.ToTable("store_table_areas");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.Description).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name }).IsUnique();
}
private static void ConfigureStoreTable(EntityTypeBuilder<StoreTable> builder)
{
builder.ToTable("store_tables");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.TableCode).HasMaxLength(32).IsRequired();
builder.Property(x => x.Tags).HasMaxLength(128);
builder.Property(x => x.QrCodeUrl).HasMaxLength(512);
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.TableCode }).IsUnique();
}
private static void ConfigureStoreEmployeeShift(EntityTypeBuilder<StoreEmployeeShift> builder)
{
builder.ToTable("store_employee_shifts");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.StaffId).IsRequired();
builder.Property(x => x.RoleType).HasConversion<int>();
builder.Property(x => x.Notes).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ShiftDate, x.StaffId }).IsUnique();
}
private static void ConfigureProductAttributeGroup(EntityTypeBuilder<ProductAttributeGroup> builder)
{
builder.ToTable("product_attribute_groups");
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.SelectionType).HasConversion<int>();
builder.Property(x => x.StoreId);
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.Name });
}
private static void ConfigureProductAttributeOption(EntityTypeBuilder<ProductAttributeOption> builder)
{
builder.ToTable("product_attribute_options");
builder.HasKey(x => x.Id);
builder.Property(x => x.AttributeGroupId).IsRequired();
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.ExtraPrice).HasPrecision(18, 2);
builder.HasIndex(x => new { x.TenantId, x.AttributeGroupId, x.Name }).IsUnique();
}
private static void ConfigureProductSku(EntityTypeBuilder<ProductSku> builder)
{
builder.ToTable("product_skus");
builder.HasKey(x => x.Id);
builder.Property(x => x.ProductId).IsRequired();
builder.Property(x => x.SkuCode).HasMaxLength(32).IsRequired();
builder.Property(x => x.Barcode).HasMaxLength(64);
builder.Property(x => x.Price).HasPrecision(18, 2);
builder.Property(x => x.OriginalPrice).HasPrecision(18, 2);
builder.Property(x => x.Weight).HasPrecision(10, 3);
builder.Property(x => x.AttributesJson).HasColumnType("text");
builder.HasIndex(x => new { x.TenantId, x.SkuCode }).IsUnique();
}
private static void ConfigureProductAddonGroup(EntityTypeBuilder<ProductAddonGroup> builder)
{
builder.ToTable("product_addon_groups");
builder.HasKey(x => x.Id);
builder.Property(x => x.ProductId).IsRequired();
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.SelectionType).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.ProductId, x.Name });
}
private static void ConfigureProductAddonOption(EntityTypeBuilder<ProductAddonOption> builder)
{
builder.ToTable("product_addon_options");
builder.HasKey(x => x.Id);
builder.Property(x => x.AddonGroupId).IsRequired();
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.ExtraPrice).HasPrecision(18, 2);
}
private static void ConfigureProductPricingRule(EntityTypeBuilder<ProductPricingRule> builder)
{
builder.ToTable("product_pricing_rules");
builder.HasKey(x => x.Id);
builder.Property(x => x.ProductId).IsRequired();
builder.Property(x => x.RuleType).HasConversion<int>();
builder.Property(x => x.ConditionsJson).HasColumnType("text").IsRequired();
builder.Property(x => x.Price).HasPrecision(18, 2);
builder.Property(x => x.WeekdaysJson).HasColumnType("text");
builder.HasIndex(x => new { x.TenantId, x.ProductId, x.RuleType });
}
private static void ConfigureProductMediaAsset(EntityTypeBuilder<ProductMediaAsset> builder)
{
builder.ToTable("product_media_assets");
builder.HasKey(x => x.Id);
builder.Property(x => x.ProductId).IsRequired();
builder.Property(x => x.MediaType).HasConversion<int>();
builder.Property(x => x.Url).HasMaxLength(512).IsRequired();
builder.Property(x => x.Caption).HasMaxLength(256);
}
private static void ConfigureInventoryItem(EntityTypeBuilder<InventoryItem> builder)
{
builder.ToTable("inventory_items");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.ProductSkuId).IsRequired();
builder.Property(x => x.BatchNumber).HasMaxLength(64);
builder.Property(x => x.Location).HasMaxLength(64);
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ProductSkuId, x.BatchNumber });
}
private static void ConfigureInventoryAdjustment(EntityTypeBuilder<InventoryAdjustment> builder)
{
builder.ToTable("inventory_adjustments");
builder.HasKey(x => x.Id);
builder.Property(x => x.InventoryItemId).IsRequired();
builder.Property(x => x.AdjustmentType).HasConversion<int>();
builder.Property(x => x.Reason).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.InventoryItemId, x.OccurredAt });
}
private static void ConfigureInventoryBatch(EntityTypeBuilder<InventoryBatch> builder)
{
builder.ToTable("inventory_batches");
builder.HasKey(x => x.Id);
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.ProductSkuId).IsRequired();
builder.Property(x => x.BatchNumber).HasMaxLength(64).IsRequired();
builder.HasIndex(x => new { x.TenantId, x.StoreId, x.ProductSkuId, x.BatchNumber }).IsUnique();
}
private static void ConfigureShoppingCart(EntityTypeBuilder<ShoppingCart> builder)
{
builder.ToTable("shopping_carts");
builder.HasKey(x => x.Id);
builder.Property(x => x.UserId).IsRequired();
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.TableContext).HasMaxLength(64);
builder.Property(x => x.DeliveryPreference).HasMaxLength(32);
builder.HasIndex(x => new { x.TenantId, x.UserId, x.StoreId }).IsUnique();
}
private static void ConfigureCartItem(EntityTypeBuilder<CartItem> builder)
{
builder.ToTable("cart_items");
builder.HasKey(x => x.Id);
builder.Property(x => x.ShoppingCartId).IsRequired();
builder.Property(x => x.ProductId).IsRequired();
builder.Property(x => x.ProductName).HasMaxLength(128).IsRequired();
builder.Property(x => x.UnitPrice).HasPrecision(18, 2);
builder.Property(x => x.Remark).HasMaxLength(256);
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.AttributesJson).HasColumnType("text");
builder.HasIndex(x => new { x.TenantId, x.ShoppingCartId });
}
private static void ConfigureCartItemAddon(EntityTypeBuilder<CartItemAddon> builder)
{
builder.ToTable("cart_item_addons");
builder.HasKey(x => x.Id);
builder.Property(x => x.CartItemId).IsRequired();
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.ExtraPrice).HasPrecision(18, 2);
}
private static void ConfigureCheckoutSession(EntityTypeBuilder<CheckoutSession> builder)
{
builder.ToTable("checkout_sessions");
builder.HasKey(x => x.Id);
builder.Property(x => x.UserId).IsRequired();
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.SessionToken).HasMaxLength(64).IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.ValidationResultJson).HasColumnType("text").IsRequired();
builder.HasIndex(x => new { x.TenantId, x.SessionToken }).IsUnique();
}
private static void ConfigureOrderStatusHistory(EntityTypeBuilder<OrderStatusHistory> builder)
{
builder.ToTable("order_status_histories");
builder.HasKey(x => x.Id);
builder.Property(x => x.OrderId).IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.Notes).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.OrderId, x.OccurredAt });
}
private static void ConfigureRefundRequest(EntityTypeBuilder<RefundRequest> builder)
{
builder.ToTable("refund_requests");
builder.HasKey(x => x.Id);
builder.Property(x => x.OrderId).IsRequired();
builder.Property(x => x.RefundNo).HasMaxLength(32).IsRequired();
builder.Property(x => x.Amount).HasPrecision(18, 2);
builder.Property(x => x.Reason).HasMaxLength(256).IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.ReviewNotes).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.RefundNo }).IsUnique();
}
private static void ConfigurePaymentRefundRecord(EntityTypeBuilder<PaymentRefundRecord> builder)
{
builder.ToTable("payment_refund_records");
builder.HasKey(x => x.Id);
builder.Property(x => x.PaymentRecordId).IsRequired();
builder.Property(x => x.OrderId).IsRequired();
builder.Property(x => x.Amount).HasPrecision(18, 2);
builder.Property(x => x.ChannelRefundId).HasMaxLength(64);
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.Payload).HasColumnType("text");
builder.HasIndex(x => new { x.TenantId, x.PaymentRecordId });
}
private static void ConfigureDeliveryEvent(EntityTypeBuilder<DeliveryEvent> builder)
{
builder.ToTable("delivery_events");
builder.HasKey(x => x.Id);
builder.Property(x => x.DeliveryOrderId).IsRequired();
builder.Property(x => x.EventType).HasConversion<int>();
builder.Property(x => x.Message).HasMaxLength(256);
builder.Property(x => x.Payload).HasColumnType("text");
builder.HasIndex(x => new { x.TenantId, x.DeliveryOrderId, x.EventType });
}
private static void ConfigureGroupOrder(EntityTypeBuilder<GroupOrder> builder)
{
builder.ToTable("group_orders");
builder.HasKey(x => x.Id);
builder.Property(x => x.GroupOrderNo).HasMaxLength(32).IsRequired();
builder.Property(x => x.GroupPrice).HasPrecision(18, 2);
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.GroupOrderNo }).IsUnique();
}
private static void ConfigureGroupParticipant(EntityTypeBuilder<GroupParticipant> builder)
{
builder.ToTable("group_participants");
builder.HasKey(x => x.Id);
builder.Property(x => x.GroupOrderId).IsRequired();
builder.Property(x => x.OrderId).IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.GroupOrderId, x.UserId }).IsUnique();
}
private static void ConfigureCouponTemplate(EntityTypeBuilder<CouponTemplate> builder)
{
builder.ToTable("coupon_templates");
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
builder.Property(x => x.CouponType).HasConversion<int>();
builder.Property(x => x.Description).HasMaxLength(512);
builder.Property(x => x.TotalQuantity);
builder.Property(x => x.StoreScopeJson).HasColumnType("text");
builder.Property(x => x.ProductScopeJson).HasColumnType("text");
builder.Property(x => x.ChannelsJson).HasColumnType("text");
builder.Property(x => x.Status).HasConversion<int>();
}
private static void ConfigureCoupon(EntityTypeBuilder<Coupon> builder)
{
builder.ToTable("coupons");
builder.HasKey(x => x.Id);
builder.Property(x => x.CouponTemplateId).IsRequired();
builder.Property(x => x.Code).HasMaxLength(32).IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
}
private static void ConfigurePromotionCampaign(EntityTypeBuilder<PromotionCampaign> builder)
{
builder.ToTable("promotion_campaigns");
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
builder.Property(x => x.PromotionType).HasConversion<int>();
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.RulesJson).HasColumnType("text").IsRequired();
builder.Property(x => x.AudienceDescription).HasMaxLength(512);
builder.Property(x => x.BannerUrl).HasMaxLength(512);
}
private static void ConfigureMemberProfile(EntityTypeBuilder<MemberProfile> builder)
{
builder.ToTable("member_profiles");
builder.HasKey(x => x.Id);
builder.Property(x => x.Mobile).HasMaxLength(32).IsRequired();
builder.Property(x => x.Nickname).HasMaxLength(64);
builder.Property(x => x.AvatarUrl).HasMaxLength(256);
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.Mobile }).IsUnique();
}
private static void ConfigureMemberTier(EntityTypeBuilder<MemberTier> builder)
{
builder.ToTable("member_tiers");
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
builder.Property(x => x.BenefitsJson).HasColumnType("text");
builder.HasIndex(x => new { x.TenantId, x.Name }).IsUnique();
}
private static void ConfigureMemberPointLedger(EntityTypeBuilder<MemberPointLedger> builder)
{
builder.ToTable("member_point_ledgers");
builder.HasKey(x => x.Id);
builder.Property(x => x.MemberId).IsRequired();
builder.Property(x => x.Reason).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.MemberId, x.OccurredAt });
}
private static void ConfigureMemberGrowthLog(EntityTypeBuilder<MemberGrowthLog> builder)
{
builder.ToTable("member_growth_logs");
builder.HasKey(x => x.Id);
builder.Property(x => x.MemberId).IsRequired();
builder.Property(x => x.Notes).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.MemberId, x.OccurredAt });
}
private static void ConfigureChatSession(EntityTypeBuilder<ChatSession> builder)
{
builder.ToTable("chat_sessions");
builder.HasKey(x => x.Id);
builder.Property(x => x.SessionCode).HasMaxLength(64).IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.SessionCode }).IsUnique();
}
private static void ConfigureChatMessage(EntityTypeBuilder<ChatMessage> builder)
{
builder.ToTable("chat_messages");
builder.HasKey(x => x.Id);
builder.Property(x => x.ChatSessionId).IsRequired();
builder.Property(x => x.SenderType).HasConversion<int>();
builder.Property(x => x.ContentType).HasMaxLength(64).IsRequired();
builder.Property(x => x.Content).HasMaxLength(1024).IsRequired();
builder.HasIndex(x => new { x.TenantId, x.ChatSessionId, x.CreatedAt });
}
private static void ConfigureSupportTicket(EntityTypeBuilder<SupportTicket> builder)
{
builder.ToTable("support_tickets");
builder.HasKey(x => x.Id);
builder.Property(x => x.TicketNo).HasMaxLength(32).IsRequired();
builder.Property(x => x.Subject).HasMaxLength(128).IsRequired();
builder.Property(x => x.Description).HasColumnType("text").IsRequired();
builder.Property(x => x.Priority).HasConversion<int>();
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.TicketNo }).IsUnique();
}
private static void ConfigureTicketComment(EntityTypeBuilder<TicketComment> builder)
{
builder.ToTable("ticket_comments");
builder.HasKey(x => x.Id);
builder.Property(x => x.SupportTicketId).IsRequired();
builder.Property(x => x.Content).HasMaxLength(1024).IsRequired();
builder.Property(x => x.AttachmentsJson).HasColumnType("text");
builder.HasIndex(x => new { x.TenantId, x.SupportTicketId });
}
private static void ConfigureAffiliatePartner(EntityTypeBuilder<AffiliatePartner> builder)
{
builder.ToTable("affiliate_partners");
builder.HasKey(x => x.Id);
builder.Property(x => x.DisplayName).HasMaxLength(64).IsRequired();
builder.Property(x => x.Phone).HasMaxLength(32);
builder.Property(x => x.ChannelType).HasConversion<int>();
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.Remarks).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.DisplayName });
}
private static void ConfigureAffiliateOrder(EntityTypeBuilder<AffiliateOrder> builder)
{
builder.ToTable("affiliate_orders");
builder.HasKey(x => x.Id);
builder.Property(x => x.AffiliatePartnerId).IsRequired();
builder.Property(x => x.OrderId).IsRequired();
builder.Property(x => x.OrderAmount).HasPrecision(18, 2);
builder.Property(x => x.EstimatedCommission).HasPrecision(18, 2);
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.AffiliatePartnerId, x.OrderId }).IsUnique();
}
private static void ConfigureAffiliatePayout(EntityTypeBuilder<AffiliatePayout> builder)
{
builder.ToTable("affiliate_payouts");
builder.HasKey(x => x.Id);
builder.Property(x => x.AffiliatePartnerId).IsRequired();
builder.Property(x => x.Period).HasMaxLength(32).IsRequired();
builder.Property(x => x.Amount).HasPrecision(18, 2);
builder.Property(x => x.Status).HasConversion<int>();
builder.Property(x => x.Remarks).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.AffiliatePartnerId, x.Period }).IsUnique();
}
private static void ConfigureCheckInCampaign(EntityTypeBuilder<CheckInCampaign> builder)
{
builder.ToTable("checkin_campaigns");
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
builder.Property(x => x.Description).HasMaxLength(512);
builder.Property(x => x.RewardsJson).HasColumnType("text").IsRequired();
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.Name });
}
private static void ConfigureCheckInRecord(EntityTypeBuilder<CheckInRecord> builder)
{
builder.ToTable("checkin_records");
builder.HasKey(x => x.Id);
builder.Property(x => x.CheckInCampaignId).IsRequired();
builder.Property(x => x.UserId).IsRequired();
builder.Property(x => x.RewardJson).HasColumnType("text").IsRequired();
builder.HasIndex(x => new { x.TenantId, x.CheckInCampaignId, x.UserId, x.CheckInDate }).IsUnique();
}
private static void ConfigureCommunityPost(EntityTypeBuilder<CommunityPost> builder)
{
builder.ToTable("community_posts");
builder.HasKey(x => x.Id);
builder.Property(x => x.AuthorUserId).IsRequired();
builder.Property(x => x.Title).HasMaxLength(128);
builder.Property(x => x.Content).HasColumnType("text").IsRequired();
builder.Property(x => x.MediaJson).HasColumnType("text");
builder.Property(x => x.Status).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.AuthorUserId, x.CreatedAt });
}
private static void ConfigureCommunityComment(EntityTypeBuilder<CommunityComment> builder)
{
builder.ToTable("community_comments");
builder.HasKey(x => x.Id);
builder.Property(x => x.PostId).IsRequired();
builder.Property(x => x.Content).HasMaxLength(512).IsRequired();
builder.HasIndex(x => new { x.TenantId, x.PostId, x.CreatedAt });
}
private static void ConfigureCommunityReaction(EntityTypeBuilder<CommunityReaction> builder)
{
builder.ToTable("community_reactions");
builder.HasKey(x => x.Id);
builder.Property(x => x.PostId).IsRequired();
builder.Property(x => x.ReactionType).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.PostId, x.UserId }).IsUnique();
}
private static void ConfigureMapLocation(EntityTypeBuilder<MapLocation> builder)
{
builder.ToTable("map_locations");
builder.HasKey(x => x.Id);
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
builder.Property(x => x.Address).HasMaxLength(256).IsRequired();
builder.Property(x => x.Landmark).HasMaxLength(128);
builder.HasIndex(x => new { x.TenantId, x.StoreId });
}
private static void ConfigureNavigationRequest(EntityTypeBuilder<NavigationRequest> builder)
{
builder.ToTable("navigation_requests");
builder.HasKey(x => x.Id);
builder.Property(x => x.UserId).IsRequired();
builder.Property(x => x.StoreId).IsRequired();
builder.Property(x => x.Channel).HasConversion<int>();
builder.Property(x => x.TargetApp).HasConversion<int>();
builder.HasIndex(x => new { x.TenantId, x.UserId, x.StoreId, x.RequestedAt });
}
private static void ConfigureMetricDefinition(EntityTypeBuilder<MetricDefinition> builder)
{
builder.ToTable("metric_definitions");
builder.HasKey(x => x.Id);
builder.Property(x => x.Code).HasMaxLength(64).IsRequired();
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
builder.Property(x => x.Description).HasMaxLength(512);
builder.Property(x => x.DimensionsJson).HasColumnType("text");
builder.Property(x => x.DefaultAggregation).HasMaxLength(32).IsRequired();
builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
}
private static void ConfigureMetricSnapshot(EntityTypeBuilder<MetricSnapshot> builder)
{
builder.ToTable("metric_snapshots");
builder.HasKey(x => x.Id);
builder.Property(x => x.MetricDefinitionId).IsRequired();
builder.Property(x => x.DimensionKey).HasMaxLength(256).IsRequired();
builder.Property(x => x.Value).HasPrecision(18, 4);
builder.HasIndex(x => new { x.TenantId, x.MetricDefinitionId, x.DimensionKey, x.WindowStart, x.WindowEnd }).IsUnique();
}
private static void ConfigureMetricAlertRule(EntityTypeBuilder<MetricAlertRule> builder)
{
builder.ToTable("metric_alert_rules");
builder.HasKey(x => x.Id);
builder.Property(x => x.MetricDefinitionId).IsRequired();
builder.Property(x => x.ConditionJson).HasColumnType("text").IsRequired();
builder.Property(x => x.Severity).HasConversion<int>();
builder.Property(x => x.NotificationChannels).HasMaxLength(256);
builder.HasIndex(x => new { x.TenantId, x.MetricDefinitionId, x.Severity });
}
}

View File

@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
@@ -12,7 +13,7 @@ internal sealed class TakeoutAppDesignTimeDbContextFactory
: DesignTimeDbContextFactoryBase<TakeoutAppDbContext>
{
public TakeoutAppDesignTimeDbContextFactory()
: base("TAKEOUTSAAS_APP_CONNECTION", "takeout_saas_app")
: base(DatabaseConstants.AppDataSource, "TAKEOUTSAAS_APP_CONNECTION")
{
}

View File

@@ -21,6 +21,7 @@ public abstract class AppDbContext(DbContextOptions options, ICurrentUserAccesso
{
base.OnModelCreating(modelBuilder);
ApplySoftDeleteQueryFilters(modelBuilder);
modelBuilder.ApplyXmlComments();
}
/// <summary>

View File

@@ -1,25 +1,33 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using TakeoutSaaS.Infrastructure.Common.Persistence;
using Microsoft.Extensions.Configuration;
using TakeoutSaaS.Infrastructure.Common.Options;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime;
/// <summary>
/// EF Core 设计时 DbContext 工厂基类,提供统一的连接串与依赖替身
/// EF Core 设计时 DbContext 工厂基类,统一读取 appsettings 中的数据库配置
/// </summary>
internal abstract class DesignTimeDbContextFactoryBase<TContext> : IDesignTimeDbContextFactory<TContext>
where TContext : TenantAwareDbContext
{
private readonly string _connectionStringEnvVar;
private readonly string _defaultDatabase;
private readonly string _dataSourceName;
private readonly string? _connectionStringEnvVar;
protected DesignTimeDbContextFactoryBase(string connectionStringEnvVar, string defaultDatabase)
protected DesignTimeDbContextFactoryBase(string dataSourceName, string? connectionStringEnvVar = null)
{
if (string.IsNullOrWhiteSpace(dataSourceName))
{
throw new ArgumentException("数据源名称不能为空。", nameof(dataSourceName));
}
_dataSourceName = dataSourceName;
_connectionStringEnvVar = connectionStringEnvVar;
_defaultDatabase = defaultDatabase;
}
public TContext CreateDbContext(string[] args)
@@ -46,15 +54,91 @@ internal abstract class DesignTimeDbContextFactoryBase<TContext> : IDesignTimeDb
private string ResolveConnectionString()
{
var env = Environment.GetEnvironmentVariable(_connectionStringEnvVar);
if (!string.IsNullOrWhiteSpace(env))
if (!string.IsNullOrWhiteSpace(_connectionStringEnvVar))
{
return env;
var envValue = Environment.GetEnvironmentVariable(_connectionStringEnvVar);
if (!string.IsNullOrWhiteSpace(envValue))
{
return envValue;
}
}
return $"Host=localhost;Port=5432;Database={_defaultDatabase};Username=postgres;Password=postgres";
var configuration = BuildConfiguration();
var writeConnection = configuration[$"{DatabaseOptions.SectionName}:DataSources:{_dataSourceName}:Write"];
if (string.IsNullOrWhiteSpace(writeConnection))
{
throw new InvalidOperationException(
$"未在配置中找到数据源 '{_dataSourceName}' 的 Write 连接字符串,请检查 appsettings 或设置 {_connectionStringEnvVar ?? ""} 环境变量。");
}
return writeConnection;
}
private static IConfigurationRoot BuildConfiguration()
{
var basePath = ResolveConfigurationDirectory();
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
return new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
.AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: false)
.AddEnvironmentVariables()
.Build();
}
private static string ResolveConfigurationDirectory()
{
var explicitDir = Environment.GetEnvironmentVariable("TAKEOUTSAAS_APPSETTINGS_DIR");
if (!string.IsNullOrWhiteSpace(explicitDir) && Directory.Exists(explicitDir))
{
return explicitDir;
}
var currentDir = Directory.GetCurrentDirectory();
var solutionRoot = LocateSolutionRoot(currentDir);
var candidateDirs = new[]
{
currentDir,
solutionRoot,
solutionRoot is null ? null : Path.Combine(solutionRoot, "src", "Api", "TakeoutSaaS.AdminApi"),
solutionRoot is null ? null : Path.Combine(solutionRoot, "src", "Api", "TakeoutSaaS.UserApi"),
solutionRoot is null ? null : Path.Combine(solutionRoot, "src", "Api", "TakeoutSaaS.MiniApi")
}.Where(dir => !string.IsNullOrWhiteSpace(dir));
foreach (var dir in candidateDirs)
{
if (dir != null && Directory.Exists(dir) && HasAppSettings(dir))
{
return dir;
}
}
throw new InvalidOperationException(
"未找到 appsettings 配置文件,请设置 TAKEOUTSAAS_APPSETTINGS_DIR 环境变量指向包含 appsettings*.json 的目录。");
}
private static string? LocateSolutionRoot(string currentPath)
{
var directoryInfo = new DirectoryInfo(currentPath);
while (directoryInfo != null)
{
if (File.Exists(Path.Combine(directoryInfo.FullName, "TakeoutSaaS.sln")))
{
return directoryInfo.FullName;
}
directoryInfo = directoryInfo.Parent;
}
return null;
}
private static bool HasAppSettings(string directory) =>
File.Exists(Path.Combine(directory, "appsettings.json")) ||
Directory.GetFiles(directory, "appsettings.*.json").Length > 0;
private sealed class DesignTimeTenantProvider : ITenantProvider
{
public Guid GetCurrentTenantId() => Guid.Empty;

View File

@@ -0,0 +1,136 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Xml.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace TakeoutSaaS.Infrastructure.Common.Persistence;
/// <summary>
/// Applies XML documentation summaries to EF Core entities/columns as comments.
/// </summary>
internal static class ModelBuilderCommentExtensions
{
public static void ApplyXmlComments(this ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
ApplyEntityComment(entityType);
}
}
private static void ApplyEntityComment(IMutableEntityType entityType)
{
var clrType = entityType.ClrType;
if (clrType == null)
{
return;
}
if (XmlDocCommentProvider.TryGetSummary(clrType, out var typeComment))
{
entityType.SetComment(typeComment);
}
foreach (var property in entityType.GetProperties())
{
var propertyInfo = property.PropertyInfo;
if (propertyInfo == null)
{
continue;
}
if (XmlDocCommentProvider.TryGetSummary(propertyInfo, out var propertyComment))
{
property.SetComment(propertyComment);
}
}
}
private static class XmlDocCommentProvider
{
private static readonly ConcurrentDictionary<Assembly, IReadOnlyDictionary<string, string>> Cache = new();
public static bool TryGetSummary(MemberInfo member, out string? summary)
{
summary = null;
var assembly = member switch
{
Type type => type.Assembly,
_ => member.DeclaringType?.Assembly
};
if (assembly == null)
{
return false;
}
var map = Cache.GetOrAdd(assembly, LoadComments);
if (map.Count == 0)
{
return false;
}
var key = GetMemberKey(member);
if (key == null || !map.TryGetValue(key, out var text))
{
return false;
}
summary = text;
return true;
}
private static IReadOnlyDictionary<string, string> LoadComments(Assembly assembly)
{
var dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
var xmlPath = Path.ChangeExtension(assembly.Location, ".xml");
if (string.IsNullOrWhiteSpace(xmlPath) || !File.Exists(xmlPath))
{
return dictionary;
}
var document = XDocument.Load(xmlPath);
foreach (var member in document.Descendants("member"))
{
var name = member.Attribute("name")?.Value;
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
var summary = member.Element("summary")?.Value;
if (string.IsNullOrWhiteSpace(summary))
{
continue;
}
var normalized = Normalize(summary);
if (!string.IsNullOrWhiteSpace(normalized))
{
dictionary[name] = normalized;
}
}
return dictionary;
}
private static string? GetMemberKey(MemberInfo member) =>
member switch
{
Type type => $"T:{GetFullName(type)}",
PropertyInfo property => $"P:{GetFullName(property.DeclaringType!)}.{property.Name}",
FieldInfo field => $"F:{GetFullName(field.DeclaringType!)}.{field.Name}",
_ => null
};
private static string GetFullName(Type type) =>
(type.FullName ?? type.Name).Replace('+', '.');
private static string Normalize(string text)
{
var chars = text.Replace('\r', ' ').Replace('\n', ' ').Replace('\t', ' ');
return string.Join(' ', chars.Split(' ', StringSplitOptions.RemoveEmptyEntries));
}
}
}

View File

@@ -28,7 +28,7 @@ public static class DictionaryServiceCollectionExtensions
public static IServiceCollection AddDictionaryInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
services.AddDatabaseInfrastructure(configuration);
services.AddPostgresDbContext<DictionaryDbContext>(DatabaseConstants.AppDataSource);
services.AddPostgresDbContext<DictionaryDbContext>(DatabaseConstants.DictionaryDataSource);
services.AddScoped<IDictionaryRepository, EfDictionaryRepository>();
services.AddScoped<IDictionaryCache, DistributedDictionaryCache>();

View File

@@ -0,0 +1,206 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using TakeoutSaaS.Infrastructure.Dictionary.Persistence;
#nullable disable
namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations
{
[DbContext(typeof(DictionaryDbContext))]
[Migration("20251201094456_AddEntityComments")]
partial class AddEntityComments
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("实体唯一标识。");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("分组编码(唯一)。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uuid")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uuid")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasComment("描述信息。");
b.Property<bool>("IsEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasComment("是否启用。");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasComment("分组名称。");
b.Property<int>("Scope")
.HasColumnType("integer")
.HasComment("分组作用域:系统/业务。");
b.Property<Guid>("TenantId")
.HasColumnType("uuid")
.HasComment("所属租户 ID。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
b.HasIndex("TenantId");
b.HasIndex("TenantId", "Code")
.IsUnique();
b.ToTable("dictionary_groups", null, t =>
{
t.HasComment("参数字典分组(系统参数、业务参数)。");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("实体唯一标识。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uuid")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uuid")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("character varying(512)")
.HasComment("描述信息。");
b.Property<Guid>("GroupId")
.HasColumnType("uuid")
.HasComment("关联分组 ID。");
b.Property<bool>("IsDefault")
.HasColumnType("boolean")
.HasComment("是否默认项。");
b.Property<bool>("IsEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true)
.HasComment("是否启用。");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("字典项键。");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(100)
.HasComment("排序值,越小越靠前。");
b.Property<Guid>("TenantId")
.HasColumnType("uuid")
.HasComment("所属租户 ID。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("字典项值。");
b.HasKey("Id");
b.HasIndex("TenantId");
b.HasIndex("GroupId", "Key")
.IsUnique();
b.ToTable("dictionary_items", null, t =>
{
t.HasComment("参数字典项。");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b =>
{
b.HasOne("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", "Group")
.WithMany("Items")
.HasForeignKey("GroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Group");
});
modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryGroup", b =>
{
b.Navigation("Items");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,599 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations
{
/// <inheritdoc />
public partial class AddEntityComments : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "dictionary_items",
comment: "参数字典项。");
migrationBuilder.AlterTable(
name: "dictionary_groups",
comment: "参数字典分组(系统参数、业务参数)。");
migrationBuilder.AlterColumn<string>(
name: "Value",
table: "dictionary_items",
type: "character varying(256)",
maxLength: 256,
nullable: false,
comment: "字典项值。",
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256);
migrationBuilder.AlterColumn<Guid>(
name: "UpdatedBy",
table: "dictionary_items",
type: "uuid",
nullable: true,
comment: "最后更新人用户标识,匿名或系统操作时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "UpdatedAt",
table: "dictionary_items",
type: "timestamp with time zone",
nullable: true,
comment: "最近一次更新时间UTC从未更新时为 null。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "TenantId",
table: "dictionary_items",
type: "uuid",
nullable: false,
comment: "所属租户 ID。",
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<int>(
name: "SortOrder",
table: "dictionary_items",
type: "integer",
nullable: false,
defaultValue: 100,
comment: "排序值,越小越靠前。",
oldClrType: typeof(int),
oldType: "integer",
oldDefaultValue: 100);
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "dictionary_items",
type: "character varying(64)",
maxLength: 64,
nullable: false,
comment: "字典项键。",
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<bool>(
name: "IsEnabled",
table: "dictionary_items",
type: "boolean",
nullable: false,
defaultValue: true,
comment: "是否启用。",
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: true);
migrationBuilder.AlterColumn<bool>(
name: "IsDefault",
table: "dictionary_items",
type: "boolean",
nullable: false,
comment: "是否默认项。",
oldClrType: typeof(bool),
oldType: "boolean");
migrationBuilder.AlterColumn<Guid>(
name: "GroupId",
table: "dictionary_items",
type: "uuid",
nullable: false,
comment: "关联分组 ID。",
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "dictionary_items",
type: "character varying(512)",
maxLength: 512,
nullable: true,
comment: "描述信息。",
oldClrType: typeof(string),
oldType: "character varying(512)",
oldMaxLength: 512,
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "DeletedBy",
table: "dictionary_items",
type: "uuid",
nullable: true,
comment: "删除人用户标识(软删除),未删除时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "DeletedAt",
table: "dictionary_items",
type: "timestamp with time zone",
nullable: true,
comment: "软删除时间UTC未删除时为 null。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "CreatedBy",
table: "dictionary_items",
type: "uuid",
nullable: true,
comment: "创建人用户标识,匿名或系统操作时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "CreatedAt",
table: "dictionary_items",
type: "timestamp with time zone",
nullable: false,
comment: "创建时间UTC。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone");
migrationBuilder.AlterColumn<Guid>(
name: "Id",
table: "dictionary_items",
type: "uuid",
nullable: false,
comment: "实体唯一标识。",
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<Guid>(
name: "UpdatedBy",
table: "dictionary_groups",
type: "uuid",
nullable: true,
comment: "最后更新人用户标识,匿名或系统操作时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "UpdatedAt",
table: "dictionary_groups",
type: "timestamp with time zone",
nullable: true,
comment: "最近一次更新时间UTC从未更新时为 null。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "TenantId",
table: "dictionary_groups",
type: "uuid",
nullable: false,
comment: "所属租户 ID。",
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<int>(
name: "Scope",
table: "dictionary_groups",
type: "integer",
nullable: false,
comment: "分组作用域:系统/业务。",
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "dictionary_groups",
type: "character varying(128)",
maxLength: 128,
nullable: false,
comment: "分组名称。",
oldClrType: typeof(string),
oldType: "character varying(128)",
oldMaxLength: 128);
migrationBuilder.AlterColumn<bool>(
name: "IsEnabled",
table: "dictionary_groups",
type: "boolean",
nullable: false,
defaultValue: true,
comment: "是否启用。",
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: true);
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "dictionary_groups",
type: "character varying(512)",
maxLength: 512,
nullable: true,
comment: "描述信息。",
oldClrType: typeof(string),
oldType: "character varying(512)",
oldMaxLength: 512,
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "DeletedBy",
table: "dictionary_groups",
type: "uuid",
nullable: true,
comment: "删除人用户标识(软删除),未删除时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "DeletedAt",
table: "dictionary_groups",
type: "timestamp with time zone",
nullable: true,
comment: "软删除时间UTC未删除时为 null。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "CreatedBy",
table: "dictionary_groups",
type: "uuid",
nullable: true,
comment: "创建人用户标识,匿名或系统操作时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "CreatedAt",
table: "dictionary_groups",
type: "timestamp with time zone",
nullable: false,
comment: "创建时间UTC。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone");
migrationBuilder.AlterColumn<string>(
name: "Code",
table: "dictionary_groups",
type: "character varying(64)",
maxLength: 64,
nullable: false,
comment: "分组编码(唯一)。",
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<Guid>(
name: "Id",
table: "dictionary_groups",
type: "uuid",
nullable: false,
comment: "实体唯一标识。",
oldClrType: typeof(Guid),
oldType: "uuid");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "dictionary_items",
oldComment: "参数字典项。");
migrationBuilder.AlterTable(
name: "dictionary_groups",
oldComment: "参数字典分组(系统参数、业务参数)。");
migrationBuilder.AlterColumn<string>(
name: "Value",
table: "dictionary_items",
type: "character varying(256)",
maxLength: 256,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldComment: "字典项值。");
migrationBuilder.AlterColumn<Guid>(
name: "UpdatedBy",
table: "dictionary_items",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "最后更新人用户标识,匿名或系统操作时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "UpdatedAt",
table: "dictionary_items",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true,
oldComment: "最近一次更新时间UTC从未更新时为 null。");
migrationBuilder.AlterColumn<Guid>(
name: "TenantId",
table: "dictionary_items",
type: "uuid",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid",
oldComment: "所属租户 ID。");
migrationBuilder.AlterColumn<int>(
name: "SortOrder",
table: "dictionary_items",
type: "integer",
nullable: false,
defaultValue: 100,
oldClrType: typeof(int),
oldType: "integer",
oldDefaultValue: 100,
oldComment: "排序值,越小越靠前。");
migrationBuilder.AlterColumn<string>(
name: "Key",
table: "dictionary_items",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldComment: "字典项键。");
migrationBuilder.AlterColumn<bool>(
name: "IsEnabled",
table: "dictionary_items",
type: "boolean",
nullable: false,
defaultValue: true,
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: true,
oldComment: "是否启用。");
migrationBuilder.AlterColumn<bool>(
name: "IsDefault",
table: "dictionary_items",
type: "boolean",
nullable: false,
oldClrType: typeof(bool),
oldType: "boolean",
oldComment: "是否默认项。");
migrationBuilder.AlterColumn<Guid>(
name: "GroupId",
table: "dictionary_items",
type: "uuid",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid",
oldComment: "关联分组 ID。");
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "dictionary_items",
type: "character varying(512)",
maxLength: 512,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(512)",
oldMaxLength: 512,
oldNullable: true,
oldComment: "描述信息。");
migrationBuilder.AlterColumn<Guid>(
name: "DeletedBy",
table: "dictionary_items",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "删除人用户标识(软删除),未删除时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "DeletedAt",
table: "dictionary_items",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true,
oldComment: "软删除时间UTC未删除时为 null。");
migrationBuilder.AlterColumn<Guid>(
name: "CreatedBy",
table: "dictionary_items",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "创建人用户标识,匿名或系统操作时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "CreatedAt",
table: "dictionary_items",
type: "timestamp with time zone",
nullable: false,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldComment: "创建时间UTC。");
migrationBuilder.AlterColumn<Guid>(
name: "Id",
table: "dictionary_items",
type: "uuid",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid",
oldComment: "实体唯一标识。");
migrationBuilder.AlterColumn<Guid>(
name: "UpdatedBy",
table: "dictionary_groups",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "最后更新人用户标识,匿名或系统操作时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "UpdatedAt",
table: "dictionary_groups",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true,
oldComment: "最近一次更新时间UTC从未更新时为 null。");
migrationBuilder.AlterColumn<Guid>(
name: "TenantId",
table: "dictionary_groups",
type: "uuid",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid",
oldComment: "所属租户 ID。");
migrationBuilder.AlterColumn<int>(
name: "Scope",
table: "dictionary_groups",
type: "integer",
nullable: false,
oldClrType: typeof(int),
oldType: "integer",
oldComment: "分组作用域:系统/业务。");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "dictionary_groups",
type: "character varying(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldMaxLength: 128,
oldComment: "分组名称。");
migrationBuilder.AlterColumn<bool>(
name: "IsEnabled",
table: "dictionary_groups",
type: "boolean",
nullable: false,
defaultValue: true,
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: true,
oldComment: "是否启用。");
migrationBuilder.AlterColumn<string>(
name: "Description",
table: "dictionary_groups",
type: "character varying(512)",
maxLength: 512,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(512)",
oldMaxLength: 512,
oldNullable: true,
oldComment: "描述信息。");
migrationBuilder.AlterColumn<Guid>(
name: "DeletedBy",
table: "dictionary_groups",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "删除人用户标识(软删除),未删除时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "DeletedAt",
table: "dictionary_groups",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true,
oldComment: "软删除时间UTC未删除时为 null。");
migrationBuilder.AlterColumn<Guid>(
name: "CreatedBy",
table: "dictionary_groups",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "创建人用户标识,匿名或系统操作时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "CreatedAt",
table: "dictionary_groups",
type: "timestamp with time zone",
nullable: false,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldComment: "创建时间UTC。");
migrationBuilder.AlterColumn<string>(
name: "Code",
table: "dictionary_groups",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldComment: "分组编码(唯一)。");
migrationBuilder.AlterColumn<Guid>(
name: "Id",
table: "dictionary_groups",
type: "uuid",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid",
oldComment: "实体唯一标识。");
}
}
}

View File

@@ -26,50 +26,63 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("实体唯一标识。");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
.HasColumnType("character varying(64)")
.HasComment("分组编码(唯一)。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("character varying(512)")
.HasComment("描述信息。");
b.Property<bool>("IsEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
.HasDefaultValue(true)
.HasComment("是否启用。");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
.HasColumnType("character varying(128)")
.HasComment("分组名称。");
b.Property<int>("Scope")
.HasColumnType("integer");
.HasColumnType("integer")
.HasComment("分组作用域:系统/业务。");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("所属租户 ID。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
@@ -78,65 +91,83 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations
b.HasIndex("TenantId", "Code")
.IsUnique();
b.ToTable("dictionary_groups", (string)null);
b.ToTable("dictionary_groups", null, t =>
{
t.HasComment("参数字典分组(系统参数、业务参数)。");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("实体唯一标识。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("character varying(512)");
.HasColumnType("character varying(512)")
.HasComment("描述信息。");
b.Property<Guid>("GroupId")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("关联分组 ID。");
b.Property<bool>("IsDefault")
.HasColumnType("boolean");
.HasColumnType("boolean")
.HasComment("是否默认项。");
b.Property<bool>("IsEnabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
.HasDefaultValue(true)
.HasComment("是否启用。");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
.HasColumnType("character varying(64)")
.HasComment("字典项键。");
b.Property<int>("SortOrder")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(100);
.HasDefaultValue(100)
.HasComment("排序值,越小越靠前。");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("所属租户 ID。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
.HasColumnType("character varying(256)")
.HasComment("字典项值。");
b.HasKey("Id");
@@ -145,7 +176,10 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Migrations
b.HasIndex("GroupId", "Key")
.IsUnique();
b.ToTable("dictionary_items", (string)null);
b.ToTable("dictionary_items", null, t =>
{
t.HasComment("参数字典项。");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Dictionary.Entities.DictionaryItem", b =>

View File

@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
@@ -12,7 +13,7 @@ internal sealed class DictionaryDesignTimeDbContextFactory
: DesignTimeDbContextFactoryBase<DictionaryDbContext>
{
public DictionaryDesignTimeDbContextFactory()
: base("TAKEOUTSAAS_APP_CONNECTION", "takeout_saas_app")
: base(DatabaseConstants.DictionaryDataSource, "TAKEOUTSAAS_DICTIONARY_CONNECTION")
{
}

View File

@@ -0,0 +1,185 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using TakeoutSaaS.Infrastructure.Identity.Persistence;
#nullable disable
namespace TakeoutSaaS.Infrastructure.Identity.Migrations
{
[DbContext(typeof(IdentityDbContext))]
[Migration("20251201094410_AddEntityComments")]
partial class AddEntityComments
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.IdentityUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("实体唯一标识。");
b.Property<string>("Account")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("登录账号。");
b.Property<string>("Avatar")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("头像地址。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uuid")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uuid")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("展示名称。");
b.Property<Guid?>("MerchantId")
.HasColumnType("uuid")
.HasComment("所属商户(平台管理员为空)。");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("密码哈希。");
b.Property<string>("Permissions")
.IsRequired()
.HasColumnType("text")
.HasComment("权限集合。");
b.Property<string>("Roles")
.IsRequired()
.HasColumnType("text")
.HasComment("角色集合。");
b.Property<Guid>("TenantId")
.HasColumnType("uuid")
.HasComment("所属租户 ID。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
b.HasIndex("TenantId");
b.HasIndex("TenantId", "Account")
.IsUnique();
b.ToTable("identity_users", null, t =>
{
t.HasComment("管理后台账户实体(平台管理员、租户管理员或商户员工)。");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasComment("实体唯一标识。");
b.Property<string>("Avatar")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasComment("头像地址。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uuid")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uuid")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Nickname")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasComment("昵称。");
b.Property<string>("OpenId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasComment("微信 OpenId。");
b.Property<Guid>("TenantId")
.HasColumnType("uuid")
.HasComment("所属租户 ID。");
b.Property<string>("UnionId")
.HasMaxLength(128)
.HasColumnType("character varying(128)")
.HasComment("微信 UnionId可能为空。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
b.HasIndex("TenantId");
b.HasIndex("TenantId", "OpenId")
.IsUnique();
b.ToTable("mini_users", null, t =>
{
t.HasComment("小程序用户实体。");
});
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,581 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TakeoutSaaS.Infrastructure.Identity.Migrations
{
/// <inheritdoc />
public partial class AddEntityComments : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "mini_users",
comment: "小程序用户实体。");
migrationBuilder.AlterTable(
name: "identity_users",
comment: "管理后台账户实体(平台管理员、租户管理员或商户员工)。");
migrationBuilder.AlterColumn<Guid>(
name: "UpdatedBy",
table: "mini_users",
type: "uuid",
nullable: true,
comment: "最后更新人用户标识,匿名或系统操作时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "UpdatedAt",
table: "mini_users",
type: "timestamp with time zone",
nullable: true,
comment: "最近一次更新时间UTC从未更新时为 null。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "UnionId",
table: "mini_users",
type: "character varying(128)",
maxLength: 128,
nullable: true,
comment: "微信 UnionId可能为空。",
oldClrType: typeof(string),
oldType: "character varying(128)",
oldMaxLength: 128,
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "TenantId",
table: "mini_users",
type: "uuid",
nullable: false,
comment: "所属租户 ID。",
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<string>(
name: "OpenId",
table: "mini_users",
type: "character varying(128)",
maxLength: 128,
nullable: false,
comment: "微信 OpenId。",
oldClrType: typeof(string),
oldType: "character varying(128)",
oldMaxLength: 128);
migrationBuilder.AlterColumn<string>(
name: "Nickname",
table: "mini_users",
type: "character varying(64)",
maxLength: 64,
nullable: false,
comment: "昵称。",
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<Guid>(
name: "DeletedBy",
table: "mini_users",
type: "uuid",
nullable: true,
comment: "删除人用户标识(软删除),未删除时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "DeletedAt",
table: "mini_users",
type: "timestamp with time zone",
nullable: true,
comment: "软删除时间UTC未删除时为 null。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "CreatedBy",
table: "mini_users",
type: "uuid",
nullable: true,
comment: "创建人用户标识,匿名或系统操作时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "CreatedAt",
table: "mini_users",
type: "timestamp with time zone",
nullable: false,
comment: "创建时间UTC。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone");
migrationBuilder.AlterColumn<string>(
name: "Avatar",
table: "mini_users",
type: "character varying(256)",
maxLength: 256,
nullable: true,
comment: "头像地址。",
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "Id",
table: "mini_users",
type: "uuid",
nullable: false,
comment: "实体唯一标识。",
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<Guid>(
name: "UpdatedBy",
table: "identity_users",
type: "uuid",
nullable: true,
comment: "最后更新人用户标识,匿名或系统操作时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "UpdatedAt",
table: "identity_users",
type: "timestamp with time zone",
nullable: true,
comment: "最近一次更新时间UTC从未更新时为 null。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "TenantId",
table: "identity_users",
type: "uuid",
nullable: false,
comment: "所属租户 ID。",
oldClrType: typeof(Guid),
oldType: "uuid");
migrationBuilder.AlterColumn<string>(
name: "Roles",
table: "identity_users",
type: "text",
nullable: false,
comment: "角色集合。",
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Permissions",
table: "identity_users",
type: "text",
nullable: false,
comment: "权限集合。",
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "PasswordHash",
table: "identity_users",
type: "character varying(256)",
maxLength: 256,
nullable: false,
comment: "密码哈希。",
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256);
migrationBuilder.AlterColumn<Guid>(
name: "MerchantId",
table: "identity_users",
type: "uuid",
nullable: true,
comment: "所属商户(平台管理员为空)。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "DisplayName",
table: "identity_users",
type: "character varying(64)",
maxLength: 64,
nullable: false,
comment: "展示名称。",
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<Guid>(
name: "DeletedBy",
table: "identity_users",
type: "uuid",
nullable: true,
comment: "删除人用户标识(软删除),未删除时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "DeletedAt",
table: "identity_users",
type: "timestamp with time zone",
nullable: true,
comment: "软删除时间UTC未删除时为 null。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
migrationBuilder.AlterColumn<Guid>(
name: "CreatedBy",
table: "identity_users",
type: "uuid",
nullable: true,
comment: "创建人用户标识,匿名或系统操作时为 null。",
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true);
migrationBuilder.AlterColumn<DateTime>(
name: "CreatedAt",
table: "identity_users",
type: "timestamp with time zone",
nullable: false,
comment: "创建时间UTC。",
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone");
migrationBuilder.AlterColumn<string>(
name: "Avatar",
table: "identity_users",
type: "character varying(256)",
maxLength: 256,
nullable: true,
comment: "头像地址。",
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Account",
table: "identity_users",
type: "character varying(64)",
maxLength: 64,
nullable: false,
comment: "登录账号。",
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64);
migrationBuilder.AlterColumn<Guid>(
name: "Id",
table: "identity_users",
type: "uuid",
nullable: false,
comment: "实体唯一标识。",
oldClrType: typeof(Guid),
oldType: "uuid");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "mini_users",
oldComment: "小程序用户实体。");
migrationBuilder.AlterTable(
name: "identity_users",
oldComment: "管理后台账户实体(平台管理员、租户管理员或商户员工)。");
migrationBuilder.AlterColumn<Guid>(
name: "UpdatedBy",
table: "mini_users",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "最后更新人用户标识,匿名或系统操作时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "UpdatedAt",
table: "mini_users",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true,
oldComment: "最近一次更新时间UTC从未更新时为 null。");
migrationBuilder.AlterColumn<string>(
name: "UnionId",
table: "mini_users",
type: "character varying(128)",
maxLength: 128,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldMaxLength: 128,
oldNullable: true,
oldComment: "微信 UnionId可能为空。");
migrationBuilder.AlterColumn<Guid>(
name: "TenantId",
table: "mini_users",
type: "uuid",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid",
oldComment: "所属租户 ID。");
migrationBuilder.AlterColumn<string>(
name: "OpenId",
table: "mini_users",
type: "character varying(128)",
maxLength: 128,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(128)",
oldMaxLength: 128,
oldComment: "微信 OpenId。");
migrationBuilder.AlterColumn<string>(
name: "Nickname",
table: "mini_users",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldComment: "昵称。");
migrationBuilder.AlterColumn<Guid>(
name: "DeletedBy",
table: "mini_users",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "删除人用户标识(软删除),未删除时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "DeletedAt",
table: "mini_users",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true,
oldComment: "软删除时间UTC未删除时为 null。");
migrationBuilder.AlterColumn<Guid>(
name: "CreatedBy",
table: "mini_users",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "创建人用户标识,匿名或系统操作时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "CreatedAt",
table: "mini_users",
type: "timestamp with time zone",
nullable: false,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldComment: "创建时间UTC。");
migrationBuilder.AlterColumn<string>(
name: "Avatar",
table: "mini_users",
type: "character varying(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldNullable: true,
oldComment: "头像地址。");
migrationBuilder.AlterColumn<Guid>(
name: "Id",
table: "mini_users",
type: "uuid",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid",
oldComment: "实体唯一标识。");
migrationBuilder.AlterColumn<Guid>(
name: "UpdatedBy",
table: "identity_users",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "最后更新人用户标识,匿名或系统操作时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "UpdatedAt",
table: "identity_users",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true,
oldComment: "最近一次更新时间UTC从未更新时为 null。");
migrationBuilder.AlterColumn<Guid>(
name: "TenantId",
table: "identity_users",
type: "uuid",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid",
oldComment: "所属租户 ID。");
migrationBuilder.AlterColumn<string>(
name: "Roles",
table: "identity_users",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "text",
oldComment: "角色集合。");
migrationBuilder.AlterColumn<string>(
name: "Permissions",
table: "identity_users",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "text",
oldComment: "权限集合。");
migrationBuilder.AlterColumn<string>(
name: "PasswordHash",
table: "identity_users",
type: "character varying(256)",
maxLength: 256,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldComment: "密码哈希。");
migrationBuilder.AlterColumn<Guid>(
name: "MerchantId",
table: "identity_users",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "所属商户(平台管理员为空)。");
migrationBuilder.AlterColumn<string>(
name: "DisplayName",
table: "identity_users",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldComment: "展示名称。");
migrationBuilder.AlterColumn<Guid>(
name: "DeletedBy",
table: "identity_users",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "删除人用户标识(软删除),未删除时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "DeletedAt",
table: "identity_users",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true,
oldComment: "软删除时间UTC未删除时为 null。");
migrationBuilder.AlterColumn<Guid>(
name: "CreatedBy",
table: "identity_users",
type: "uuid",
nullable: true,
oldClrType: typeof(Guid),
oldType: "uuid",
oldNullable: true,
oldComment: "创建人用户标识,匿名或系统操作时为 null。");
migrationBuilder.AlterColumn<DateTime>(
name: "CreatedAt",
table: "identity_users",
type: "timestamp with time zone",
nullable: false,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldComment: "创建时间UTC。");
migrationBuilder.AlterColumn<string>(
name: "Avatar",
table: "identity_users",
type: "character varying(256)",
maxLength: 256,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(256)",
oldMaxLength: 256,
oldNullable: true,
oldComment: "头像地址。");
migrationBuilder.AlterColumn<string>(
name: "Account",
table: "identity_users",
type: "character varying(64)",
maxLength: 64,
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldComment: "登录账号。");
migrationBuilder.AlterColumn<Guid>(
name: "Id",
table: "identity_users",
type: "uuid",
nullable: false,
oldClrType: typeof(Guid),
oldType: "uuid",
oldComment: "实体唯一标识。");
}
}
}

View File

@@ -26,58 +26,73 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("实体唯一标识。");
b.Property<string>("Account")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
.HasColumnType("character varying(64)")
.HasComment("登录账号。");
b.Property<string>("Avatar")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
.HasColumnType("character varying(256)")
.HasComment("头像地址。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
.HasColumnType("character varying(64)")
.HasComment("展示名称。");
b.Property<Guid?>("MerchantId")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("所属商户(平台管理员为空)。");
b.Property<string>("PasswordHash")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
.HasColumnType("character varying(256)")
.HasComment("密码哈希。");
b.Property<string>("Permissions")
.IsRequired()
.HasColumnType("text");
.HasColumnType("text")
.HasComment("权限集合。");
b.Property<string>("Roles")
.IsRequired()
.HasColumnType("text");
.HasColumnType("text")
.HasComment("角色集合。");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("所属租户 ID。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
@@ -86,53 +101,68 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations
b.HasIndex("TenantId", "Account")
.IsUnique();
b.ToTable("identity_users", (string)null);
b.ToTable("identity_users", null, t =>
{
t.HasComment("管理后台账户实体(平台管理员、租户管理员或商户员工)。");
});
});
modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("实体唯一标识。");
b.Property<string>("Avatar")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
.HasColumnType("character varying(256)")
.HasComment("头像地址。");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("创建时间UTC。");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("创建人用户标识,匿名或系统操作时为 null。");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("软删除时间UTC未删除时为 null。");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("删除人用户标识(软删除),未删除时为 null。");
b.Property<string>("Nickname")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
.HasColumnType("character varying(64)")
.HasComment("昵称。");
b.Property<string>("OpenId")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
.HasColumnType("character varying(128)")
.HasComment("微信 OpenId。");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("所属租户 ID。");
b.Property<string>("UnionId")
.HasMaxLength(128)
.HasColumnType("character varying(128)");
.HasColumnType("character varying(128)")
.HasComment("微信 UnionId可能为空。");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
.HasColumnType("timestamp with time zone")
.HasComment("最近一次更新时间UTC从未更新时为 null。");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uuid");
.HasColumnType("uuid")
.HasComment("最后更新人用户标识,匿名或系统操作时为 null。");
b.HasKey("Id");
@@ -141,7 +171,10 @@ namespace TakeoutSaaS.Infrastructure.Identity.Migrations
b.HasIndex("TenantId", "OpenId")
.IsUnique();
b.ToTable("mini_users", (string)null);
b.ToTable("mini_users", null, t =>
{
t.HasComment("小程序用户实体。");
});
});
#pragma warning restore 612, 618
}

View File

@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using TakeoutSaaS.Infrastructure.Common.Persistence.DesignTime;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
@@ -12,7 +13,7 @@ internal sealed class IdentityDesignTimeDbContextFactory
: DesignTimeDbContextFactoryBase<IdentityDbContext>
{
public IdentityDesignTimeDbContextFactory()
: base("TAKEOUTSAAS_IDENTITY_CONNECTION", "takeout_saas_identity")
: base(DatabaseConstants.IdentityDataSource, "TAKEOUTSAAS_IDENTITY_CONNECTION")
{
}