diff --git a/docs/design/HLD-系统概要设计.md b/docs/design/HLD-系统概要设计.md index 44dee16..1ac8fc3 100644 --- a/docs/design/HLD-系统概要设计.md +++ b/docs/design/HLD-系统概要设计.md @@ -84,59 +84,59 @@ │ 业务范围 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 会员管理 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 会员注册 • 会员卡管理 • 权益管理 • 等级管理 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 会员管理 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 会员注册 • 会员卡管理 • 权益管理 • 等级管理 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 预约管理 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 团课预约 • 私教预约 • 场地预约 • 线上课程 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 预约管理 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 团课预约 • 私教预约 • 场地预约 • 线上课程 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 签到管理 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 扫码签到 • 刷脸签到 • NFC签到 • 教练代签 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 签到管理 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 扫码签到 • 刷脸签到 • NFC签到 • 教练代签 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 课程管理 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 课程类型 • 课程排期 • 场地管理 • 价格配置 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 课程管理 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 课程类型 • 课程排期 • 场地管理 • 价格配置 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 教练管理 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 教练信息 • 排班管理 • 课时统计 • 评价管理 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 教练管理 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 教练信息 • 排班管理 • 课时统计 • 评价管理 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 财务管理 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 营收统计 • 账单管理 • 退款管理 • 对账管理 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 财务管理 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 营收统计 • 账单管理 • 退款管理 • 对账管理 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 计划中心 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 训练计划 • 课程排期 • 会员目标 • 教练排班 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 计划中心 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 训练计划 • 课程排期 • 会员目标 • 教练排班 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 数据分析 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 会员分析 • 课程分析 • 财务分析 • 运营分析 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 数据分析 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 会员分析 • 课程分析 • 财务分析 • 运营分析 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 系统管理 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 租户管理 • 门店管理 • 权限管理 • 系统配置 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 系统管理 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 租户管理 • 门店管理 • 权限管理 • 系统配置 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -154,51 +154,51 @@ │ 总体架构 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 客户端层 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 会员小程序 (uniapp+Vue3) │ │ -│ │ • 教练端App (uniapp+Vue3) │ │ -│ │ • 管理后台PC (Vue3+Vite) │ │ -│ │ • 硬件设备 (人脸/NFC) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 客户端层 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 会员小程序 (uniapp+Vue3) │ │ +│ │ • 教练端App (uniapp+Vue3) │ │ +│ │ • 管理后台PC (Vue3+Vite) │ │ +│ │ • 硬件设备 (人脸/NFC) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ API Gateway 统一网关 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 路由转发 • 认证鉴权 • 限流熔断 • 日志追踪 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ API Gateway 统一网关 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 路由转发 • 认证鉴权 • 限流熔断 • 日志追踪 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 业务层 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 会员服务 (Member Service) │ │ -│ │ • 预约服务 (Booking Service) │ │ -│ │ • 数据服务 (Data Service) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 业务层 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 会员服务 (Member Service) │ │ +│ │ • 预约服务 (Booking Service) │ │ +│ │ • 数据服务 (Data Service) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 公共服务层 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 认证服务 • 消息服务 • 文件服务 • 缓存服务 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 公共服务层 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 认证服务 • 消息服务 • 文件服务 • 缓存服务 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 基础设施层 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • PostgreSQL • R2DBC • Caffeine • Redis(可选) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 基础设施层 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • PostgreSQL • R2DBC • Caffeine • Redis(可选) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 外部服务层 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 微信开放平台 • 短信服务 • 支付服务 • OSS存储 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 外部服务层 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 微信开放平台 • 短信服务 • 支付服务 • OSS存储 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -210,38 +210,38 @@ │ 技术架构 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 表现层 Presentation │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 会员端 uniapp (Vue3 + TS + Pinia + uni-ui) │ │ -│ │ • 教练端 uniapp (Vue3 + TS + Pinia + uni-ui) │ │ -│ │ • 管理后台 Vue3 (Vue3 + TS + Pinia + Element Plus) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 表现层 Presentation │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 会员端 uniapp (Vue3 + TS + Pinia + uni-ui) │ │ +│ │ • 教练端 uniapp (Vue3 + TS + Pinia + uni-ui) │ │ +│ │ • 管理后台 Vue3 (Vue3 + TS + Pinia + Element Plus) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 网关层 Gateway │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ Spring Cloud Gateway (路由转发/认证鉴权/限流熔断/日志追踪/灰度发布) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 网关层 Gateway │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ Spring Cloud Gateway (路由转发/认证鉴权/限流熔断/日志追踪/灰度发布) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 业务层 Business │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ Spring Boot 3 + WebFlux + JDK 21 │ │ -│ │ • Controller (API) • Service (业务逻辑) │ │ -│ │ • Repository (数据访问) • Model (领域模型) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 业务层 Business │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ Spring Boot 3 + WebFlux + JDK 21 │ │ +│ │ • Controller (API) • Service (业务逻辑) │ │ +│ │ • Repository (数据访问) • Model (领域模型) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 数据层 Data │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • PostgreSQL (R2DBC + Flyway) │ │ -│ │ • Caffeine (本地缓存 + 热点数据) │ │ -│ │ • Redis可选 (分布式缓存 + 分布式锁) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 数据层 Data │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • PostgreSQL (R2DBC + Flyway) │ │ +│ │ • Caffeine (本地缓存 + 热点数据) │ │ +│ │ • Redis可选 (分布式缓存 + 分布式锁) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -253,33 +253,33 @@ │ 部署架构 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 负载均衡器 (Nginx/ALB) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 负载均衡器 (Nginx/ALB) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ API Gateway 集群 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • API Gateway 实例1 • API Gateway 实例2 • API Gateway 实例N │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ API Gateway 集群 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • API Gateway 实例1 • API Gateway 实例2 • API Gateway 实例N │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 应用服务集群 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 应用服务 实例1 • 应用服务 实例2 • 应用服务 实例N │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 应用服务集群 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 应用服务 实例1 • 应用服务 实例2 • 应用服务 实例N │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ -│ ┌───────────────┴───────────────┐ │ -│ ▼ ▼ │ -│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ -│ │ PostgreSQL 数据库层 │ │ Redis 缓存层 │ │ -│ ├─────────────────────────┤ ├─────────────────────────┤ │ -│ │ • 主库 │ │ • 主节点 │ │ -│ │ • 从库1 (主从复制) │ │ • 从节点1 (主从复制) │ │ -│ │ • 从库N (主从复制) │ │ • 从节点N (主从复制) │ │ -│ └─────────────────────────┘ └─────────────────────────┘ │ +│ ┌───────────────┴───────────────┐ │ +│ ▼ ▼ │ +│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ +│ │ PostgreSQL 数据库层 │ │ Redis 缓存层 │ │ +│ ├─────────────────────────┤ ├─────────────────────────┤ │ +│ │ • 主库 │ │ • 主节点 │ │ +│ │ • 从库1 (主从复制) │ │ • 从节点1 (主从复制) │ │ +│ │ • 从库N (主从复制) │ │ • 从节点N (主从复制) │ │ +│ └─────────────────────────┘ └─────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -295,52 +295,52 @@ │ gym-manage-server 父工程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ gym-common 公共模块 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • gym-common-core (核心工具类、常量、枚举) │ │ -│ │ • gym-common-redis (Redis配置可选) │ │ -│ │ • gym-common-security (安全认证公共组件) │ │ -│ │ • gym-common-log (日志公共组件) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ gym-common 公共模块 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • gym-common-core (核心工具类、常量、枚举) │ │ +│ │ • gym-common-redis (Redis配置可选) │ │ +│ │ • gym-common-security (安全认证公共组件) │ │ +│ │ • gym-common-log (日志公共组件) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ gym-api API网关模块 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • controller (HTTP接口) • dto (数据传输对象) │ │ -│ │ • vo (视图对象) • config (API配置) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ gym-api API网关模块 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • controller (HTTP接口) • dto (数据传输对象) │ │ +│ │ • vo (视图对象) • config (API配置) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ gym-service 业务服务模块 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • gym-service-member (会员服务) │ │ -│ │ • gym-service-booking (预约服务) │ │ -│ │ • gym-service-checkin (签到服务) │ │ -│ │ • gym-service-course (课程服务) │ │ -│ │ • gym-service-coach (教练服务) │ │ -│ │ • gym-service-finance (财务服务) │ │ -│ │ • gym-service-data (数据服务) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ gym-service 业务服务模块 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • gym-service-member (会员服务) │ │ +│ │ • gym-service-booking (预约服务) │ │ +│ │ • gym-service-checkin (签到服务) │ │ +│ │ • gym-service-course (课程服务) │ │ +│ │ • gym-service-coach (教练服务) │ │ +│ │ • gym-service-finance (财务服务) │ │ +│ │ • gym-service-data (数据服务) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ gym-domain 领域模型模块 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • model (领域模型) • event (领域事件) • service (领域服务) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ gym-domain 领域模型模块 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • model (领域模型) • event (领域事件) • service (领域服务) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ gym-infrastructure 基础设施模块 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • repository (数据仓储) • cache (缓存配置) │ │ -│ │ • external (外部服务集成) • config (基础配置) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ gym-infrastructure 基础设施模块 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • repository (数据仓储) • cache (缓存配置) │ │ +│ │ • external (外部服务集成) • config (基础配置) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ gym-starter 启动模块 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • gym-admin (管理后台启动器) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ gym-starter 启动模块 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • gym-admin (管理后台启动器) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -499,58 +499,58 @@ HTTP方法语义: │ 接口分组 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 认证接口 /v1/auth │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • POST /login (登录) • POST /logout (登出) │ │ -│ │ • POST /refresh (刷新Token) • POST /wechat-login (微信登录) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 认证接口 /v1/auth │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • POST /login (登录) • POST /logout (登出) │ │ +│ │ • POST /refresh (刷新Token) • POST /wechat-login (微信登录) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 会员接口 /v1/members │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • GET / (会员列表) • GET /{id} (会员详情) │ │ -│ │ • POST / (创建会员) • PUT /{id} (更新会员) │ │ -│ │ • GET /{id}/cards (会员卡列表) • GET /{id}/benefits (权益列表)│ │ -│ │ • GET /{id}/bookings (预约记录) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 会员接口 /v1/members │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • GET / (会员列表) • GET /{id} (会员详情) │ │ +│ │ • POST / (创建会员) • PUT /{id} (更新会员) │ │ +│ │ • GET /{id}/cards (会员卡列表) • GET /{id}/benefits (权益列表)│ │ +│ │ • GET /{id}/bookings (预约记录) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 课程接口 /v1/courses │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • GET / (课程列表) • GET /{id} (课程详情) │ │ -│ │ • POST / (创建课程) • PUT /{id} (更新课程) │ │ -│ │ • GET /{id}/slots (可预约时段) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 课程接口 /v1/courses │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • GET / (课程列表) • GET /{id} (课程详情) │ │ +│ │ • POST / (创建课程) • PUT /{id} (更新课程) │ │ +│ │ • GET /{id}/slots (可预约时段) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 预约接口 /v1/bookings │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • GET / (预约列表) • GET /{id} (预约详情) │ │ -│ │ • POST / (创建预约) • POST /{id}/cancel (取消预约) │ │ -│ │ • GET /my (我的预约) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 预约接口 /v1/bookings │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • GET / (预约列表) • GET /{id} (预约详情) │ │ +│ │ • POST / (创建预约) • POST /{id}/cancel (取消预约) │ │ +│ │ • GET /my (我的预约) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 签到接口 /v1/checkins │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • GET / (签到列表) • POST /scan (扫码签到) │ │ -│ │ • POST /manual (手动签到) • GET /my (我的签到) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 签到接口 /v1/checkins │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • GET / (签到列表) • POST /scan (扫码签到) │ │ +│ │ • POST /manual (手动签到) • GET /my (我的签到) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 教练接口 /v1/coaches │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • GET / (教练列表) • GET /{id} (教练详情) │ │ -│ │ • GET /{id}/schedule (教练排班) • GET /{id}/slots (可预约时段)│ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 教练接口 /v1/coaches │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • GET / (教练列表) • GET /{id} (教练详情) │ │ +│ │ • GET /{id}/schedule (教练排班) • GET /{id}/slots (可预约时段)│ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 数据看板 /v1/dashboard │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • GET /overview (今日概览) • GET /trends (趋势数据) │ │ -│ │ • GET /rankings (排行数据) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 数据看板 /v1/dashboard │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • GET /overview (今日概览) • GET /trends (趋势数据) │ │ +│ │ • GET /rankings (排行数据) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` diff --git a/docs/design/LLD-会员模块详细设计.md b/docs/design/LLD-会员模块详细设计.md index 026b464..da17699 100644 --- a/docs/design/LLD-会员模块详细设计.md +++ b/docs/design/LLD-会员模块详细设计.md @@ -10,9 +10,9 @@ ## 文档修订历史 -| 版本 | 日期 | 作者 | 修订内容 | -|------|------|------|---------| -| v1.0 | 2026-02-28 | 张翔 | 初稿 | +| 版本 | 日期 | 作者 | 修订内容 | +| ---- | ---------- | ---- | -------- | +| v1.0 | 2026-02-28 | 张翔 | 初稿 | --- @@ -44,29 +44,29 @@ │ 会员模块边界 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 会员模块内部 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 会员管理 • 会员卡管理 • 权益管理 • 等级管理 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 会员模块内部 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 会员管理 • 会员卡管理 • 权益管理 • 等级管理 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 外部依赖 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 租户模块 (获取租户信息) │ │ -│ │ • 门店模块 (获取门店信息) │ │ -│ │ • 认证模块 (用户登录认证) │ │ -│ │ • 消息模块 (发送短信验证码) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 外部依赖 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 租户模块 (获取租户信息) │ │ +│ │ • 门店模块 (获取门店信息) │ │ +│ │ • 认证模块 (用户登录认证) │ │ +│ │ • 消息模块 (发送短信验证码) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 被依赖 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 预约模块 (查询会员权益、扣减权益) │ │ -│ │ • 签到模块 (查询会员信息、扣减权益) │ │ -│ │ • 财务模块 (查询会员消费记录) │ │ -│ │ • 数据模块 (会员数据分析) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 被依赖 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 预约模块 (查询会员权益、扣减权益) │ │ +│ │ • 签到模块 (查询会员信息、扣减权益) │ │ +│ │ • 财务模块 (查询会员消费记录) │ │ +│ │ • 数据模块 (会员数据分析) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -82,43 +82,43 @@ │ 实体关系图 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌──────────────┐ │ -│ │ tenant │ │ -│ │ (租户表) │ │ -│ └──────┬───────┘ │ -│ │ 1:N │ -│ ┌────┴────┐ │ -│ ▼ ▼ │ -│ ┌──────┐ ┌──────┐ │ -│ │store │ │member│ │ -│ │(门店)│ │(会员)│ │ -│ └──┬───┘ └──┬───┘ │ -│ │ 1:N │ 1:N │ -│ │ │ │ -│ │ └─────────────┐ │ -│ │ │ │ -│ │ ▼ │ -│ │ ┌──────────────────┐ │ -│ │ │ member_card │ │ -│ │ │ (会员卡) │ │ -│ │ └────────┬─────────┘ │ -│ │ │ N:1 │ -│ │ ▼ │ -│ │ ┌──────────────────┐ │ -│ │ │ card_type │ │ -│ │ │ (卡类型) │ │ -│ │ └────────┬─────────┘ │ -│ │ │ 1:N │ -│ │ ▼ │ -│ │ ┌──────────────────┐ │ -│ │ │ level_rule │ │ -│ │ │ (等级规则) │ │ -│ │ └──────────────────┘ │ -│ │ │ -│ │ ┌──────────────────┐ │ -│ └───────────────────────┤ member_benefit │ │ -│ │ (会员权益) │ │ -│ └──────────────────┘ │ +│ ┌──────────────┐ │ +│ │ tenant │ │ +│ │ (租户表) │ │ +│ └──────┬───────┘ │ +│ │ 1:N │ +│ ┌────┴────┐ │ +│ ▼ ▼ │ +│ ┌──────┐ ┌──────┐ │ +│ │store │ │member│ │ +│ │(门店)│ │(会员)│ │ +│ └──┬───┘ └──┬───┘ │ +│ │ 1:N │ 1:N │ +│ │ │ │ +│ │ └─────────────┐ │ +│ │ │ │ +│ │ ▼ │ +│ │ ┌──────────────────┐ │ +│ │ │ member_card │ │ +│ │ │ (会员卡) │ │ +│ │ └────────┬─────────┘ │ +│ │ │ N:1 │ +│ │ ▼ │ +│ │ ┌──────────────────┐ │ +│ │ │ card_type │ │ +│ │ │ (卡类型) │ │ +│ │ └────────┬─────────┘ │ +│ │ │ 1:N │ +│ │ ▼ │ +│ │ ┌──────────────────┐ │ +│ │ │ level_rule │ │ +│ │ │ (等级规则) │ │ +│ │ └──────────────────┘ │ +│ │ │ +│ │ ┌──────────────────┐ │ +│ └───────────────────────┤ member_benefit │ │ +│ │ (会员权益) │ │ +│ └──────────────────┘ │ │ │ │ 关系说明: │ │ • tenant (1) ─── (N) store : 一个租户有多个门店 │ @@ -164,7 +164,7 @@ CREATE TABLE member ( created_by BIGINT, updated_by BIGINT, deleted_at TIMESTAMP DEFAULT NULL, - + CONSTRAINT uk_member_no UNIQUE (tenant_id, member_no), CONSTRAINT uk_member_phone UNIQUE (tenant_id, phone), CONSTRAINT fk_member_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id), @@ -207,7 +207,7 @@ CREATE TABLE card_type ( created_by BIGINT, updated_by BIGINT, deleted_at TIMESTAMP DEFAULT NULL, - + CONSTRAINT uk_card_type_code UNIQUE (tenant_id, code), CONSTRAINT fk_card_type_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id) ); @@ -241,7 +241,7 @@ CREATE TABLE member_card ( created_by BIGINT, updated_by BIGINT, deleted_at TIMESTAMP DEFAULT NULL, - + CONSTRAINT uk_member_card_no UNIQUE (tenant_id, card_no), CONSTRAINT fk_member_card_member FOREIGN KEY (member_id) REFERENCES member(id), CONSTRAINT fk_member_card_type FOREIGN KEY (card_type_id) REFERENCES card_type(id) @@ -274,7 +274,7 @@ CREATE TABLE member_benefit ( created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), deleted_at TIMESTAMP DEFAULT NULL, - + CONSTRAINT fk_benefit_member FOREIGN KEY (member_id) REFERENCES member(id), CONSTRAINT fk_benefit_card FOREIGN KEY (card_id) REFERENCES member_card(id) ); @@ -328,7 +328,7 @@ CREATE TABLE level_rule ( created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), deleted_at TIMESTAMP DEFAULT NULL, - + CONSTRAINT uk_level_rule UNIQUE (tenant_id, level), CONSTRAINT fk_level_rule_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id) ); @@ -370,76 +370,76 @@ CREATE INDEX idx_exp_log_created ON exp_log(created_at); │ 会员领域模型 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ <> │ │ -│ │ Member │ │ -│ ├───────────────────────────────────────────────────────────────────┤ │ -│ │ - id: Long │ │ -│ │ - tenantId: Long │ │ -│ │ - storeId: Long │ │ -│ │ - memberNo: String │ │ -│ │ - name: String │ │ -│ │ - phone: String │ │ -│ │ - avatar: String │ │ -│ │ - gender: Gender │ │ -│ │ - birthday: LocalDate │ │ -│ │ - level: Integer │ │ -│ │ - exp: Integer │ │ -│ │ - status: MemberStatus │ │ -│ │ - cards: List │ │ -│ │ - benefits: List │ │ -│ ├───────────────────────────────────────────────────────────────────┤ │ -│ │ + activate(): void │ │ -│ │ + freeze(reason: String): void │ │ -│ │ + unfreeze(): void │ │ -│ │ + addExp(exp: Integer): void │ │ -│ │ + canLevelUp(): Boolean │ │ -│ │ + levelUp(): void │ │ -│ │ + getValidBenefits(): List │ │ -│ │ + getUsableBenefit(type: BenefitType): MemberBenefit │ │ -│ └───────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ │ 1:N │ -│ ▼ │ -│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ -│ │ <> │ │ <> │ │ -│ │ MemberCard │ │ MemberBenefit │ │ -│ ├────────────────────────────┤ ├────────────────────────────┤ │ -│ │ - id: Long │ │ - id: Long │ │ -│ │ - memberId: Long │ │ - memberId: Long │ │ -│ │ - cardTypeId: Long │ │ - cardId: Long │ │ -│ │ - cardNo: String │ │ - type: BenefitType │ │ -│ │ - status: CardStatus │ │ - category: BenefitCategory│ │ -│ │ - startDate: LocalDate │ │ - value: BigDecimal │ │ -│ │ - endDate: LocalDate │ │ - usedValue: BigDecimal │ │ -│ │ - price: BigDecimal │ │ - remainValue: BigDecimal │ │ -│ ├────────────────────────────┤ │ - expireDate: LocalDate │ │ -│ │ + activate(): void │ │ - status: BenefitStatus │ │ -│ │ + freeze(): void │ ├────────────────────────────┤ │ -│ │ + unfreeze(): void │ │ + deduct(value): void │ │ -│ │ + isExpired(): Boolean │ │ + add(value): void │ │ -│ │ + isUsable(): Boolean │ │ + isExpired(): Boolean │ │ -│ │ + getRemainDays(): Integer │ │ + isUsable(): Boolean │ │ -│ └────────────────────────────┘ └────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ <> │ │ +│ │ Member │ │ +│ ├───────────────────────────────────────────────────────────────────┤ │ +│ │ - id: Long │ │ +│ │ - tenantId: Long │ │ +│ │ - storeId: Long │ │ +│ │ - memberNo: String │ │ +│ │ - name: String │ │ +│ │ - phone: String │ │ +│ │ - avatar: String │ │ +│ │ - gender: Gender │ │ +│ │ - birthday: LocalDate │ │ +│ │ - level: Integer │ │ +│ │ - exp: Integer │ │ +│ │ - status: MemberStatus │ │ +│ │ - cards: List │ │ +│ │ - benefits: List │ │ +│ ├───────────────────────────────────────────────────────────────────┤ │ +│ │ + activate(): void │ │ +│ │ + freeze(reason: String): void │ │ +│ │ + unfreeze(): void │ │ +│ │ + addExp(exp: Integer): void │ │ +│ │ + canLevelUp(): Boolean │ │ +│ │ + levelUp(): void │ │ +│ │ + getValidBenefits(): List │ │ +│ │ + getUsableBenefit(type: BenefitType): MemberBenefit │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ 1:N │ +│ ▼ │ +│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ +│ │ <> │ │ <> │ │ +│ │ MemberCard │ │ MemberBenefit │ │ +│ ├────────────────────────────┤ ├────────────────────────────┤ │ +│ │ - id: Long │ │ - id: Long │ │ +│ │ - memberId: Long │ │ - memberId: Long │ │ +│ │ - cardTypeId: Long │ │ - cardId: Long │ │ +│ │ - cardNo: String │ │ - type: BenefitType │ │ +│ │ - status: CardStatus │ │ - category: BenefitCategory│ │ +│ │ - startDate: LocalDate │ │ - value: BigDecimal │ │ +│ │ - endDate: LocalDate │ │ - usedValue: BigDecimal │ │ +│ │ - price: BigDecimal │ │ - remainValue: BigDecimal │ │ +│ ├────────────────────────────┤ │ - expireDate: LocalDate │ │ +│ │ + activate(): void │ │ - status: BenefitStatus │ │ +│ │ + freeze(): void │ ├────────────────────────────┤ │ +│ │ + unfreeze(): void │ │ + deduct(value): void │ │ +│ │ + isExpired(): Boolean │ │ + add(value): void │ │ +│ │ + isUsable(): Boolean │ │ + isExpired(): Boolean │ │ +│ │ + getRemainDays(): Integer │ │ + isUsable(): Boolean │ │ +│ └────────────────────────────┘ └────────────────────────────┘ │ │ │ -│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ -│ │ <> │ │ <> │ │ -│ │ MemberStatus │ │ BenefitType │ │ -│ ├────────────────────────────┤ ├────────────────────────────┤ │ -│ │ NORMAL(1, "正常") │ │ DURATION(1, "时长") │ │ -│ │ FROZEN(2, "冻结") │ │ TIMES(2, "次数") │ │ -│ │ CANCELLED(3, "注销") │ │ STORED_VALUE(3, "储值") │ │ -│ └────────────────────────────┘ │ LEVEL(4, "等级") │ │ -│ └────────────────────────────┘ │ +│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ +│ │ <> │ │ <> │ │ +│ │ MemberStatus │ │ BenefitType │ │ +│ ├────────────────────────────┤ ├────────────────────────────┤ │ +│ │ NORMAL(1, "正常") │ │ DURATION(1, "时长") │ │ +│ │ FROZEN(2, "冻结") │ │ TIMES(2, "次数") │ │ +│ │ CANCELLED(3, "注销") │ │ STORED_VALUE(3, "储值") │ │ +│ └────────────────────────────┘ │ LEVEL(4, "等级") │ │ +│ └────────────────────────────┘ │ │ │ -│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ -│ │ <> │ │ <> │ │ -│ │ CardStatus │ │ BenefitCategory │ │ -│ ├────────────────────────────┤ ├────────────────────────────┤ │ -│ │ INACTIVE(1, "未激活") │ │ GROUP_CLASS(1, "团课") │ │ -│ │ ACTIVE(2, "有效") │ │ PRIVATE(2, "私教") │ │ -│ │ EXPIRED(3, "已过期") │ │ GENERAL(3, "通用") │ │ -│ │ USED_UP(4, "已用完") │ └────────────────────────────┘ │ +│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ +│ │ <> │ │ <> │ │ +│ │ CardStatus │ │ BenefitCategory │ │ +│ ├────────────────────────────┤ ├────────────────────────────┤ │ +│ │ INACTIVE(1, "未激活") │ │ GROUP_CLASS(1, "团课") │ │ +│ │ ACTIVE(2, "有效") │ │ PRIVATE(2, "私教") │ │ +│ │ EXPIRED(3, "已过期") │ │ GENERAL(3, "通用") │ │ +│ │ USED_UP(4, "已用完") │ └────────────────────────────┘ │ │ │ FROZEN(5, "已冻结") │ │ │ └────────────────────────────┘ │ │ │ @@ -453,37 +453,37 @@ CREATE INDEX idx_exp_log_created ON exp_log(created_at); │ 领域服务设计 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ <> │ │ -│ │ MemberDomainService │ │ -│ ├───────────────────────────────────────────────────────────────────┤ │ -│ │ + registerMember(command: RegisterMemberCommand): Member │ │ -│ │ + updateMemberInfo(memberId: Long, command: UpdateMemberCommand) │ │ -│ │ + freezeMember(memberId: Long, reason: String): void │ │ -│ │ + unfreezeMember(memberId: Long): void │ │ -│ │ + calculateLevel(memberId: Long): Integer │ │ -│ └───────────────────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ <> │ │ +│ │ MemberDomainService │ │ +│ ├───────────────────────────────────────────────────────────────────┤ │ +│ │ + registerMember(command: RegisterMemberCommand): Member │ │ +│ │ + updateMemberInfo(memberId: Long, command: UpdateMemberCommand) │ │ +│ │ + freezeMember(memberId: Long, reason: String): void │ │ +│ │ + unfreezeMember(memberId: Long): void │ │ +│ │ + calculateLevel(memberId: Long): Integer │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ <> │ │ -│ │ BenefitDomainService │ │ -│ ├───────────────────────────────────────────────────────────────────┤ │ -│ │ + purchaseCard(command: PurchaseCardCommand): MemberCard │ │ -│ │ + activateCard(cardId: Long): void │ │ -│ │ + deductBenefit(memberId: Long, request: DeductRequest): void │ │ -│ │ + refundBenefit(memberId: Long, request: RefundRequest): void │ │ -│ │ + expireBenefits(): void │ │ -│ │ + getUsableBenefits(memberId: Long, type: BenefitType): List │ │ -│ └───────────────────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ <> │ │ +│ │ BenefitDomainService │ │ +│ ├───────────────────────────────────────────────────────────────────┤ │ +│ │ + purchaseCard(command: PurchaseCardCommand): MemberCard │ │ +│ │ + activateCard(cardId: Long): void │ │ +│ │ + deductBenefit(memberId: Long, request: DeductRequest): void │ │ +│ │ + refundBenefit(memberId: Long, request: RefundRequest): void │ │ +│ │ + expireBenefits(): void │ │ +│ │ + getUsableBenefits(memberId: Long, type: BenefitType): List │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ <> │ │ -│ │ LevelDomainService │ │ -│ ├───────────────────────────────────────────────────────────────────┤ │ -│ │ + addExp(memberId: Long, exp: Integer, source: String): void │ │ -│ │ + calculateLevel(tenantId: Long, exp: Integer): Integer │ │ -│ │ + getLevelBenefits(tenantId: Long, level: Integer): LevelBenefit │ │ -│ └───────────────────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ <> │ │ +│ │ LevelDomainService │ │ +│ ├───────────────────────────────────────────────────────────────────┤ │ +│ │ + addExp(memberId: Long, exp: Integer, source: String): void │ │ +│ │ + calculateLevel(tenantId: Long, exp: Integer): Integer │ │ +│ │ + getLevelBenefits(tenantId: Long, level: Integer): LevelBenefit │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -499,35 +499,35 @@ CREATE INDEX idx_exp_log_created ON exp_log(created_at); │ 会员注册流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ 会员端 API层 Service层 数据层 │ -│ │ │ │ │ │ -│ │ 1.输入手机号 │ │ │ │ -│ │─────────────────▶│ │ │ │ -│ │ │ 2.发送验证码 │ │ │ -│ │ │───────────────────▶│ │ │ -│ │ │ │ 3.调用短信服务 │ │ -│ │ │ │─────────────────▶│ │ -│ │ │ │◀─────────────────│ │ -│ │ │◀───────────────────│ │ │ -│ │◀─────────────────│ 返回验证码ID │ │ │ -│ │ │ │ │ │ -│ │ 4.提交注册信息 │ │ │ │ -│ │─────────────────▶│ │ │ │ -│ │ │ 5.验证验证码 │ │ │ -│ │ │───────────────────▶│ │ │ -│ │ │ │ 6.查询手机号 │ │ -│ │ │ │─────────────────▶│ │ -│ │ │ │◀─────────────────│ │ -│ │ │ │ 7.检查是否已注册 │ │ -│ │ │ │ │ │ -│ │ │ │ 8.生成会员编号 │ │ -│ │ │ │ 9.创建会员 │ │ -│ │ │ │─────────────────▶│ │ -│ │ │ │◀─────────────────│ │ -│ │ │ │ 10.生成JWT Token │ │ -│ │ │◀───────────────────│ │ │ -│ │◀─────────────────│ 返回Token和会员信息 │ │ │ -│ │ │ │ │ │ +│ 会员端 API层 Service层 数据层 │ +│ │ │ │ │ │ +│ │ 1.输入手机号 │ │ │ │ +│ │─────────────────▶│ │ │ │ +│ │ │ 2.发送验证码 │ │ │ +│ │ │───────────────────▶│ │ │ +│ │ │ │ 3.调用短信服务 │ │ +│ │ │ │─────────────────▶│ │ +│ │ │ │◀─────────────────│ │ +│ │ │◀───────────────────│ │ │ +│ │◀─────────────────│ 返回验证码ID │ │ │ +│ │ │ │ │ │ +│ │ 4.提交注册信息 │ │ │ │ +│ │─────────────────▶│ │ │ │ +│ │ │ 5.验证验证码 │ │ │ +│ │ │───────────────────▶│ │ │ +│ │ │ │ 6.查询手机号 │ │ +│ │ │ │─────────────────▶│ │ +│ │ │ │◀─────────────────│ │ +│ │ │ │ 7.检查是否已注册 │ │ +│ │ │ │ │ │ +│ │ │ │ 8.生成会员编号 │ │ +│ │ │ │ 9.创建会员 │ │ +│ │ │ │─────────────────▶│ │ +│ │ │ │◀─────────────────│ │ +│ │ │ │ 10.生成JWT Token │ │ +│ │ │◀───────────────────│ │ │ +│ │◀─────────────────│ 返回Token和会员信息 │ │ │ +│ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -539,35 +539,35 @@ CREATE INDEX idx_exp_log_created ON exp_log(created_at); │ 会员卡购买流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ 会员端 API层 OrderService MemberService PaymentService │ -│ │ │ │ │ │ │ -│ │ 1.选择卡种│ │ │ │ │ -│ │─────────▶│ │ │ │ │ -│ │ │ 2.创建订单 │ │ │ │ -│ │ │───────────▶│ │ │ │ -│ │ │ │ 3.校验卡种 │ │ │ -│ │ │ │─────────────▶│ │ │ -│ │ │ │◀─────────────│ │ │ -│ │ │ │ 4.创建支付单 │ │ │ -│ │ │ │─────────────────────────────▶│ │ -│ │ │ │◀─────────────────────────────│ │ -│ │◀────────│ 返回支付参数│ │ │ │ -│ │ │ │ │ │ │ -│ │ 5.完成支付│ │ │ │ │ -│ │──────────────────────────────────────────────────▶│ │ -│ │ │ │ │ 6.支付回调 │ │ -│ │ │ │◀─────────────────────────────│ │ -│ │ │ │ 7.更新订单状态│ │ │ -│ │ │ │─────────────▶│ │ │ -│ │ │ │ │ 8.创建会员卡 │ │ -│ │ │ │ │─────────────▶ │ │ -│ │ │ │ │ 9.创建权益 │ │ -│ │ │ │ │─────────────▶ │ │ -│ │ │ │ │ 10.增加经验值 │ │ -│ │ │ │ │─────────────▶ │ │ -│ │ │ │◀─────────────│ │ │ -│ │◀────────│ 购买成功通知│ │ │ │ -│ │ │ │ │ │ │ +│ 会员端 API层 OrderService MemberService PaymentService │ +│ │ │ │ │ │ │ +│ │ 1.选择卡种│ │ │ │ │ +│ │─────────▶│ │ │ │ │ +│ │ │ 2.创建订单 │ │ │ │ +│ │ │───────────▶│ │ │ │ +│ │ │ │ 3.校验卡种 │ │ │ +│ │ │ │─────────────▶│ │ │ +│ │ │ │◀─────────────│ │ │ +│ │ │ │ 4.创建支付单 │ │ │ +│ │ │ │─────────────────────────────▶│ │ +│ │ │ │◀─────────────────────────────│ │ +│ │◀────────│ 返回支付参数│ │ │ │ +│ │ │ │ │ │ │ +│ │ 5.完成支付│ │ │ │ │ +│ │──────────────────────────────────────────────────▶│ │ +│ │ │ │ │ 6.支付回调 │ │ +│ │ │ │◀─────────────────────────────│ │ +│ │ │ │ 7.更新订单状态│ │ │ +│ │ │ │─────────────▶│ │ │ +│ │ │ │ │ 8.创建会员卡 │ │ +│ │ │ │ │─────────────▶ │ │ +│ │ │ │ │ 9.创建权益 │ │ +│ │ │ │ │─────────────▶ │ │ +│ │ │ │ │ 10.增加经验值 │ │ +│ │ │ │ │─────────────▶ │ │ +│ │ │ │◀─────────────│ │ │ +│ │◀────────│ 购买成功通知│ │ │ │ +│ │ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -579,35 +579,35 @@ CREATE INDEX idx_exp_log_created ON exp_log(created_at); │ 权益扣减流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ 调用方 BenefitService Repository │ -│ │ │ │ │ -│ │ 1.请求扣减权益 │ │ │ -│ │─────────────────▶│ │ │ -│ │ │ 2.查询可用权益 │ │ -│ │ │────────────────────▶│ │ -│ │ │◀────────────────────│ │ -│ │ │ │ │ -│ │ │ 3.按优先级排序 │ │ -│ │ │ (即将过期优先) │ │ -│ │ │ │ │ -│ │ │ 4.校验余额充足 │ │ -│ │ │ │ │ -│ │ │ 5.执行扣减(事务) │ │ -│ │ │────────────────────▶│ │ -│ │ │ UPDATE member_benefit │ -│ │ │ SET remain_value = remain_value - ? │ -│ │ │ used_value = used_value + ? │ -│ │ │ WHERE id = ? AND remain_value >= ? │ -│ │ │ │ │ -│ │ │ 6.记录变更日志 │ │ -│ │ │────────────────────▶│ │ -│ │ │ │ │ -│ │ │ 7.检查是否用完 │ │ -│ │ │ 更新状态 │ │ -│ │ │────────────────────▶│ │ -│ │ │ │ │ -│ │◀─────────────────│ 返回扣减结果 │ │ -│ │ │ │ │ +│ 调用方 BenefitService Repository │ +│ │ │ │ │ +│ │ 1.请求扣减权益 │ │ │ +│ │─────────────────▶│ │ │ +│ │ │ 2.查询可用权益 │ │ +│ │ │────────────────────▶│ │ +│ │ │◀────────────────────│ │ +│ │ │ │ │ +│ │ │ 3.按优先级排序 │ │ +│ │ │ (即将过期优先) │ │ +│ │ │ │ │ +│ │ │ 4.校验余额充足 │ │ +│ │ │ │ │ +│ │ │ 5.执行扣减(事务) │ │ +│ │ │────────────────────▶│ │ +│ │ │ UPDATE member_benefit │ +│ │ │ SET remain_value = remain_value - ? │ +│ │ │ used_value = used_value + ? │ +│ │ │ WHERE id = ? AND remain_value >= ? │ +│ │ │ │ │ +│ │ │ 6.记录变更日志 │ │ +│ │ │────────────────────▶│ │ +│ │ │ │ │ +│ │ │ 7.检查是否用完 │ │ +│ │ │ 更新状态 │ │ +│ │ │────────────────────▶│ │ +│ │ │ │ │ +│ │◀─────────────────│ 返回扣减结果 │ │ +│ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -619,35 +619,35 @@ CREATE INDEX idx_exp_log_created ON exp_log(created_at); │ 等级升级流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ 触发源 LevelService Member LevelRule │ -│ │ │ │ │ │ -│ │ 1.增加经验值 │ │ │ │ -│ │────────────────▶│ │ │ │ -│ │ │ 2.查询当前会员 │ │ │ -│ │ │───────────────────▶│ │ │ -│ │ │◀───────────────────│ │ │ -│ │ │ │ │ │ -│ │ │ 3.计算新等级 │ │ │ -│ │ │─────────────────────────────────────▶│ │ -│ │ │◀─────────────────────────────────────│ │ -│ │ │ │ │ │ -│ │ │ 4.比较是否升级 │ │ │ -│ │ │ │ │ │ -│ │ │ [如果升级] │ │ │ -│ │ │ 5.更新会员等级 │ │ │ -│ │ │───────────────────▶│ │ │ -│ │ │ │ │ │ -│ │ │ 6.发放升级奖励 │ │ │ -│ │ │ (经验值/优惠券) │ │ │ -│ │ │───────────────────▶│ │ │ -│ │ │ │ │ │ -│ │ │ 7.记录升级日志 │ │ │ -│ │ │───────────────────▶│ │ │ -│ │ │ │ │ │ -│ │ │ 8.发送升级通知 │ │ │ -│ │ │ │ │ │ -│ │◀────────────────│ 返回升级结果 │ │ │ -│ │ │ │ │ │ +│ 触发源 LevelService Member LevelRule │ +│ │ │ │ │ │ +│ │ 1.增加经验值 │ │ │ │ +│ │────────────────▶│ │ │ │ +│ │ │ 2.查询当前会员 │ │ │ +│ │ │───────────────────▶│ │ │ +│ │ │◀───────────────────│ │ │ +│ │ │ │ │ │ +│ │ │ 3.计算新等级 │ │ │ +│ │ │─────────────────────────────────────▶│ │ +│ │ │◀─────────────────────────────────────│ │ +│ │ │ │ │ │ +│ │ │ 4.比较是否升级 │ │ │ +│ │ │ │ │ │ +│ │ │ [如果升级] │ │ │ +│ │ │ 5.更新会员等级 │ │ │ +│ │ │───────────────────▶│ │ │ +│ │ │ │ │ │ +│ │ │ 6.发放升级奖励 │ │ │ +│ │ │ (经验值/优惠券) │ │ │ +│ │ │───────────────────▶│ │ │ +│ │ │ │ │ │ +│ │ │ 7.记录升级日志 │ │ │ +│ │ │───────────────────▶│ │ │ +│ │ │ │ │ │ +│ │ │ 8.发送升级通知 │ │ │ +│ │ │ │ │ │ +│ │◀────────────────│ 返回升级结果 │ │ │ +│ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -983,7 +983,7 @@ import java.util.List; @Getter @Setter public class Member extends BaseEntity implements AggregateRoot { - + private Long tenantId; private Long storeId; private String memberNo; @@ -1003,18 +1003,18 @@ public class Member extends BaseEntity implements AggregateRoot { private String registerSource; private LocalDateTime lastLoginAt; private String lastLoginIp; - + private List cards = new ArrayList<>(); private List benefits = new ArrayList<>(); - + public boolean isNormal() { return MemberStatus.NORMAL.equals(this.status); } - + public boolean isFrozen() { return MemberStatus.FROZEN.equals(this.status); } - + public void freeze(String reason) { if (!isNormal()) { throw new BusinessException("会员状态异常,无法冻结"); @@ -1022,7 +1022,7 @@ public class Member extends BaseEntity implements AggregateRoot { this.status = MemberStatus.FROZEN; this.updatedAt = LocalDateTime.now(); } - + public void unfreeze() { if (!isFrozen()) { throw new BusinessException("会员未冻结"); @@ -1030,7 +1030,7 @@ public class Member extends BaseEntity implements AggregateRoot { this.status = MemberStatus.NORMAL; this.updatedAt = LocalDateTime.now(); } - + public void addExp(Integer exp) { if (exp <= 0) { return; @@ -1039,20 +1039,20 @@ public class Member extends BaseEntity implements AggregateRoot { this.totalExp += exp; this.updatedAt = LocalDateTime.now(); } - + public void updateLevel(Integer newLevel) { if (newLevel > this.level) { this.level = newLevel; this.updatedAt = LocalDateTime.now(); } } - + public List getValidBenefits() { return benefits.stream() .filter(MemberBenefit::isUsable) .toList(); } - + public List getUsableBenefits(BenefitType type, BenefitCategory category) { return benefits.stream() .filter(b -> b.getType().equals(type)) @@ -1084,7 +1084,7 @@ import java.time.LocalDateTime; @Getter @Setter public class MemberBenefit extends BaseEntity { - + private Long tenantId; private Long memberId; private Long cardId; @@ -1099,7 +1099,7 @@ public class MemberBenefit extends BaseEntity { private BenefitStatus status; private String source; private Long sourceId; - + public boolean isUsable() { if (!BenefitStatus.VALID.equals(status)) { return false; @@ -1112,15 +1112,15 @@ public class MemberBenefit extends BaseEntity { } return true; } - + public boolean isExpired() { return expireDate != null && expireDate.isBefore(LocalDate.now()); } - + public boolean canDeduct(BigDecimal amount) { return remainValue.compareTo(amount) >= 0; } - + public void deduct(BigDecimal amount) { if (!canDeduct(amount)) { throw new BusinessException("权益余额不足"); @@ -1128,18 +1128,18 @@ public class MemberBenefit extends BaseEntity { this.usedValue = this.usedValue.add(amount); this.remainValue = this.remainValue.subtract(amount); this.updatedAt = LocalDateTime.now(); - + if (this.remainValue.compareTo(BigDecimal.ZERO) == 0) { this.status = BenefitStatus.USED_UP; } } - + public void add(BigDecimal amount) { this.value = this.value.add(amount); this.remainValue = this.remainValue.add(amount); this.updatedAt = LocalDateTime.now(); } - + public void expire() { this.status = BenefitStatus.EXPIRED; this.updatedAt = LocalDateTime.now(); @@ -1166,10 +1166,10 @@ import java.time.LocalDateTime; @Service @RequiredArgsConstructor public class BenefitDomainService { - + private final MemberBenefitRepository benefitRepository; private final BenefitLogRepository benefitLogRepository; - + public Flux getUsableBenefits(Long memberId, BenefitType type, BenefitCategory category) { return benefitRepository.findUsableByMemberId(memberId, type, category) .sort((a, b) -> { @@ -1179,24 +1179,24 @@ public class BenefitDomainService { return a.getExpireDate().compareTo(b.getExpireDate()); }); } - + @Transactional - public Mono deductBenefit(Long memberId, BenefitType type, BenefitCategory category, + public Mono deductBenefit(Long memberId, BenefitType type, BenefitCategory category, BigDecimal amount, String bizType, Long bizId, String reason) { return getUsableBenefits(memberId, type, category) .collectList() .flatMap(benefits -> { BigDecimal remaining = amount; - + for (MemberBenefit benefit : benefits) { if (remaining.compareTo(BigDecimal.ZERO) <= 0) break; - + BigDecimal deductAmount = benefit.getRemainValue().min(remaining); BigDecimal beforeValue = benefit.getRemainValue(); - + benefit.deduct(deductAmount); remaining = remaining.subtract(deductAmount); - + BenefitLog log = BenefitLog.builder() .tenantId(benefit.getTenantId()) .memberId(memberId) @@ -1209,23 +1209,23 @@ public class BenefitDomainService { .bizType(bizType) .bizId(bizId) .build(); - + benefitLogRepository.save(log).subscribe(); } - + if (remaining.compareTo(BigDecimal.ZERO) > 0) { return Mono.error(new BusinessException("权益余额不足")); } - + return Mono.when(benefits.stream() .map(benefitRepository::save) .toArray(Mono[]::new)); }); } - + @Transactional - public Mono addBenefit(Long memberId, Long cardId, BenefitType type, - BenefitCategory category, String name, BigDecimal value, + public Mono addBenefit(Long memberId, Long cardId, BenefitType type, + BenefitCategory category, String name, BigDecimal value, String unit, LocalDate expireDate, String source, Long sourceId) { MemberBenefit benefit = new MemberBenefit(); benefit.setMemberId(memberId); @@ -1241,7 +1241,7 @@ public class BenefitDomainService { benefit.setStatus(BenefitStatus.VALID); benefit.setSource(source); benefit.setSourceId(sourceId); - + return benefitRepository.save(benefit) .doOnNext(saved -> { BenefitLog log = BenefitLog.builder() @@ -1256,7 +1256,7 @@ public class BenefitDomainService { .bizType("purchase") .bizId(sourceId) .build(); - + benefitLogRepository.save(log).subscribe(); }); } @@ -1280,26 +1280,26 @@ import java.time.LocalDateTime; @Repository @RequiredArgsConstructor public class MemberRepository { - + private final MemberR2dbcRepository r2dbcRepository; private final DatabaseClient databaseClient; - + public Mono findById(Long id) { return r2dbcRepository.findByIdAndDeletedAtIsNull(id); } - + public Mono findByPhone(Long tenantId, String phone) { return r2dbcRepository.findByTenantIdAndPhoneAndDeletedAtIsNull(tenantId, phone); } - + public Mono findByMemberNo(Long tenantId, String memberNo) { return r2dbcRepository.findByTenantIdAndMemberNoAndDeletedAtIsNull(tenantId, memberNo); } - + public Flux findByStoreId(Long storeId) { return r2dbcRepository.findByStoreIdAndDeletedAtIsNull(storeId); } - + public Mono save(Member member) { member.setUpdatedAt(LocalDateTime.now()); if (member.getId() == null) { @@ -1308,10 +1308,10 @@ public class MemberRepository { } return r2dbcRepository.save(member); } - + public Mono softDelete(Long id, Long operatorId) { return databaseClient.sql(""" - UPDATE member + UPDATE member SET deleted_at = NOW(), updated_at = NOW(), updated_by = :operatorId WHERE id = :id AND deleted_at IS NULL """) @@ -1321,22 +1321,22 @@ public class MemberRepository { .rowsUpdated() .then(); } - + public Mono existsByPhone(Long tenantId, String phone) { return r2dbcRepository.existsByTenantIdAndPhoneAndDeletedAtIsNull(tenantId, phone); } - + public Mono countByStoreId(Long storeId) { return r2dbcRepository.countByStoreIdAndDeletedAtIsNull(storeId); } - + public Mono generateMemberNo(Long tenantId) { String prefix = "M" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); - + return databaseClient.sql(""" SELECT COALESCE(MAX(CAST(SUBSTRING(member_no, 10) AS BIGINT)), 0) + 1 as next_no - FROM member - WHERE tenant_id = :tenantId + FROM member + WHERE tenant_id = :tenantId AND member_no LIKE :prefix AND deleted_at IS NULL """) @@ -1410,7 +1410,7 @@ import java.util.concurrent.TimeUnit; @Configuration public class CacheConfig { - + @Bean public Cache memberCache() { return Caffeine.newBuilder() @@ -1419,7 +1419,7 @@ public class CacheConfig { .recordStats() .build(); } - + @Bean public Cache benefitCache() { return Caffeine.newBuilder() @@ -1428,7 +1428,7 @@ public class CacheConfig { .recordStats() .build(); } - + @Bean public Cache cardTypeCache() { return Caffeine.newBuilder() @@ -1437,7 +1437,7 @@ public class CacheConfig { .recordStats() .build(); } - + @Bean public Cache levelRuleCache() { return Caffeine.newBuilder() @@ -1459,56 +1459,56 @@ public class CacheConfig { package com.gym.domain.exception; public class MemberException extends BusinessException { - - public static final MemberException MEMBER_NOT_FOUND = + + public static final MemberException MEMBER_NOT_FOUND = new MemberException(40001, "会员不存在"); - - public static final MemberException MEMBER_ALREADY_EXISTS = + + public static final MemberException MEMBER_ALREADY_EXISTS = new MemberException(40002, "会员已存在"); - - public static final MemberException MEMBER_FROZEN = + + public static final MemberException MEMBER_FROZEN = new MemberException(40003, "会员已冻结"); - - public static final MemberException MEMBER_CANCELLED = + + public static final MemberException MEMBER_CANCELLED = new MemberException(40004, "会员已注销"); - - public static final MemberException PHONE_ALREADY_REGISTERED = + + public static final MemberException PHONE_ALREADY_REGISTERED = new MemberException(40005, "手机号已注册"); - - public static final MemberException VERIFY_CODE_ERROR = + + public static final MemberException VERIFY_CODE_ERROR = new MemberException(40006, "验证码错误"); - - public static final MemberException VERIFY_CODE_EXPIRED = + + public static final MemberException VERIFY_CODE_EXPIRED = new MemberException(40007, "验证码已过期"); - + public MemberException(int code, String message) { super(code, message); } } public class BenefitException extends BusinessException { - - public static final BenefitException BENEFIT_NOT_FOUND = + + public static final BenefitException BENEFIT_NOT_FOUND = new BenefitException(40101, "权益不存在"); - - public static final BenefitException BENEFIT_INSUFFICIENT = + + public static final BenefitException BENEFIT_INSUFFICIENT = new BenefitException(40102, "权益余额不足"); - - public static final BenefitException BENEFIT_EXPIRED = + + public static final BenefitException BENEFIT_EXPIRED = new BenefitException(40103, "权益已过期"); - - public static final BenefitException BENEFIT_USED_UP = + + public static final BenefitException BENEFIT_USED_UP = new BenefitException(40104, "权益已用完"); - - public static final BenefitException CARD_NOT_FOUND = + + public static final BenefitException CARD_NOT_FOUND = new BenefitException(40105, "会员卡不存在"); - - public static final BenefitException CARD_EXPIRED = + + public static final BenefitException CARD_EXPIRED = new BenefitException(40106, "会员卡已过期"); - - public static final BenefitException CARD_FROZEN = + + public static final BenefitException CARD_FROZEN = new BenefitException(40107, "会员卡已冻结"); - + public BenefitException(int code, String message) { super(code, message); } @@ -1530,25 +1530,25 @@ import reactor.core.publisher.Mono; @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { - + @ExceptionHandler(MemberException.class) public Mono> handleMemberException(MemberException e) { log.warn("会员业务异常: code={}, message={}", e.getCode(), e.getMessage()); return Mono.just(ApiResponse.error(e.getCode(), e.getMessage())); } - + @ExceptionHandler(BenefitException.class) public Mono> handleBenefitException(BenefitException e) { log.warn("权益业务异常: code={}, message={}", e.getCode(), e.getMessage()); return Mono.just(ApiResponse.error(e.getCode(), e.getMessage())); } - + @ExceptionHandler(BusinessException.class) public Mono> handleBusinessException(BusinessException e) { log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage()); return Mono.just(ApiResponse.error(e.getCode(), e.getMessage())); } - + @ExceptionHandler(Exception.class) public Mono> handleException(Exception e) { log.error("系统异常", e); @@ -1572,37 +1572,37 @@ import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.*; class MemberBenefitTest { - + @Test void testIsUsable_WhenValid_ShouldReturnTrue() { MemberBenefit benefit = new MemberBenefit(); benefit.setStatus(BenefitStatus.VALID); benefit.setRemainValue(BigDecimal.TEN); benefit.setExpireDate(LocalDate.now().plusDays(10)); - + assertTrue(benefit.isUsable()); } - + @Test void testIsUsable_WhenExpired_ShouldReturnFalse() { MemberBenefit benefit = new MemberBenefit(); benefit.setStatus(BenefitStatus.VALID); benefit.setRemainValue(BigDecimal.TEN); benefit.setExpireDate(LocalDate.now().minusDays(1)); - + assertFalse(benefit.isUsable()); } - + @Test void testIsUsable_WhenUsedUp_ShouldReturnFalse() { MemberBenefit benefit = new MemberBenefit(); benefit.setStatus(BenefitStatus.VALID); benefit.setRemainValue(BigDecimal.ZERO); benefit.setExpireDate(LocalDate.now().plusDays(10)); - + assertFalse(benefit.isUsable()); } - + @Test void testDeduct_WhenSufficient_ShouldSuccess() { MemberBenefit benefit = new MemberBenefit(); @@ -1610,22 +1610,22 @@ class MemberBenefitTest { benefit.setValue(BigDecimal.TEN); benefit.setUsedValue(BigDecimal.ZERO); benefit.setRemainValue(BigDecimal.TEN); - + benefit.deduct(BigDecimal.valueOf(3)); - + assertEquals(BigDecimal.valueOf(7), benefit.getRemainValue()); assertEquals(BigDecimal.valueOf(3), benefit.getUsedValue()); } - + @Test void testDeduct_WhenInsufficient_ShouldThrowException() { MemberBenefit benefit = new MemberBenefit(); benefit.setStatus(BenefitStatus.VALID); benefit.setRemainValue(BigDecimal.ONE); - + assertThrows(BusinessException.class, () -> benefit.deduct(BigDecimal.TEN)); } - + @Test void testDeduct_WhenFullyUsed_ShouldUpdateStatus() { MemberBenefit benefit = new MemberBenefit(); @@ -1633,9 +1633,9 @@ class MemberBenefitTest { benefit.setValue(BigDecimal.TEN); benefit.setUsedValue(BigDecimal.ZERO); benefit.setRemainValue(BigDecimal.TEN); - + benefit.deduct(BigDecimal.TEN); - + assertEquals(BenefitStatus.USED_UP, benefit.getStatus()); } } @@ -1659,25 +1659,25 @@ import java.time.LocalDate; @SpringBootTest class BenefitDomainServiceIntegrationTest { - + @Autowired private BenefitDomainService benefitService; - + @Autowired private MemberBenefitRepository benefitRepository; - + @Autowired private TransactionalOperator rxtx; - + @Test void testDeductBenefit_ShouldSuccess() { Long memberId = 1L; Long benefitId = 1L; - + Mono result = benefitRepository.findById(benefitId) .flatMap(benefit -> { BigDecimal beforeValue = benefit.getRemainValue(); - + return benefitService.deductBenefit( memberId, BenefitType.TIMES, @@ -1695,7 +1695,7 @@ class BenefitDomainServiceIntegrationTest { })) .as(rxtx::transactional) .then(); - + StepVerifier.create(result) .verifyComplete(); } @@ -1713,7 +1713,7 @@ public enum MemberStatus { NORMAL(1, "正常"), FROZEN(2, "冻结"), CANCELLED(3, "注销"); - + private final int code; private final String name; } @@ -1724,7 +1724,7 @@ public enum CardStatus { EXPIRED(3, "已过期"), USED_UP(4, "已用完"), FROZEN(5, "已冻结"); - + private final int code; private final String name; } @@ -1734,7 +1734,7 @@ public enum BenefitType { TIMES(2, "次数"), STORED_VALUE(3, "储值"), LEVEL(4, "等级"); - + private final int code; private final String name; } @@ -1743,7 +1743,7 @@ public enum BenefitCategory { GROUP_CLASS(1, "团课"), PRIVATE(2, "私教"), GENERAL(3, "通用"); - + private final int code; private final String name; } @@ -1752,7 +1752,7 @@ public enum BenefitStatus { VALID(1, "有效"), EXPIRED(2, "已过期"), USED_UP(3, "已用完"); - + private final int code; private final String name; } @@ -1763,7 +1763,7 @@ public enum BenefitLogType { EXPIRE(3, "过期"), FREEZE(4, "冻结"), UNFREEZE(5, "解冻"); - + private final int code; private final String name; } @@ -1772,7 +1772,7 @@ public enum Gender { UNKNOWN(0, "未知"), MALE(1, "男"), FEMALE(2, "女"); - + private final int code; private final String name; } @@ -1786,11 +1786,11 @@ member: default-level: 0 default-exp: 0 member-no-prefix: "M" - + benefit: expire-notice-days: 7 max-benefits-per-member: 100 - + level: exp-rules: checkin: 10 @@ -1802,10 +1802,10 @@ member: ## 十一、版本历史 -| 版本 | 日期 | 作者 | 变更内容 | -|------|------|------|---------| -| v1.0 | 2026-02-28 | 张翔 | 初稿 | +| 版本 | 日期 | 作者 | 变更内容 | +| ---- | ---------- | ---- | -------- | +| v1.0 | 2026-02-28 | 张翔 | 初稿 | --- -*文档结束* +_文档结束_ diff --git a/docs/design/LLD-签到模块详细设计.md b/docs/design/LLD-签到模块详细设计.md index fe4ada7..e083f27 100644 --- a/docs/design/LLD-签到模块详细设计.md +++ b/docs/design/LLD-签到模块详细设计.md @@ -10,9 +10,9 @@ ## 文档修订历史 -| 版本 | 日期 | 作者 | 修订内容 | -|------|------|------|---------| -| v1.0 | 2026-02-28 | 张翔 | 初稿 | +| 版本 | 日期 | 作者 | 修订内容 | +| ---- | ---------- | ---- | -------- | +| v1.0 | 2026-02-28 | 张翔 | 初稿 | --- @@ -44,41 +44,41 @@ │ 签到模块边界 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 签到模块内部 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 签到网关 • 签到验证 • 签到记录 • 签到统计 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 签到模块内部 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 签到网关 • 签到验证 • 签到记录 • 签到统计 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 外部依赖 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 会员模块 (查询会员信息、验证会员状态) │ │ -│ │ • 权益模块 (验证权益有效性、扣减权益) │ │ -│ │ • 预约模块 (查询预约信息、验证签到资格) │ │ -│ │ • 设备模块 (人脸识别设备、NFC读卡器) │ │ -│ │ • 消息模块 (发送签到通知) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 外部依赖 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 会员模块 (查询会员信息、验证会员状态) │ │ +│ │ • 权益模块 (验证权益有效性、扣减权益) │ │ +│ │ • 预约模块 (查询预约信息、验证签到资格) │ │ +│ │ • 设备模块 (人脸识别设备、NFC读卡器) │ │ +│ │ • 消息模块 (发送签到通知) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 被依赖 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 财务模块 (签到消费记录) │ │ -│ │ • 数据模块 (签到数据分析、会员活跃度统计) │ │ -│ │ • 考勤模块 (教练考勤统计) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 被依赖 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 财务模块 (签到消费记录) │ │ +│ │ • 数据模块 (签到数据分析、会员活跃度统计) │ │ +│ │ • 考勤模块 (教练考勤统计) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### 1.3 签到类型 -| 签到类型 | 说明 | 触发条件 | 验证规则 | -|---------|------|---------|---------| -| **入场签到** | 会员进入健身房 | 扫码/人脸/NFC | 验证会员卡有效性 | +| 签到类型 | 说明 | 触发条件 | 验证规则 | +| ------------ | ---------------- | ------------- | ---------------------- | +| **入场签到** | 会员进入健身房 | 扫码/人脸/NFC | 验证会员卡有效性 | | **课程签到** | 会员参加预约课程 | 扫码/教练代签 | 验证预约记录、时间窗口 | -| **私教签到** | 会员上私教课 | 教练代签 | 验证私教预约、教练身份 | -| **活动签到** | 会员参加活动 | 扫码 | 验证活动报名 | +| **私教签到** | 会员上私教课 | 教练代签 | 验证私教预约、教练身份 | +| **活动签到** | 会员参加活动 | 扫码 | 验证活动报名 | --- @@ -91,19 +91,19 @@ │ 实体关系图 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ │ -│ │ member │ │ booking_record │ │ device │ │ -│ │ (会员) │ │ (预约记录) │ │ (设备) │ │ -│ └──────┬───────┘ └────────┬─────────┘ └──────┬───────┘ │ -│ │ 1:N │ 1:N │ 1:N │ -│ │ │ │ │ -│ └───────────────────┴─────────────────────┘ │ -│ │ 1:N │ -│ ▼ │ -│ ┌──────────────────┐ │ -│ │ checkin_record │ │ -│ │ (签到记录) │ │ -│ └──────────────────┘ │ +│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ │ +│ │ member │ │ booking_record │ │ device │ │ +│ │ (会员) │ │ (预约记录) │ │ (设备) │ │ +│ └──────┬───────┘ └────────┬─────────┘ └──────┬───────┘ │ +│ │ 1:N │ 1:N │ 1:N │ +│ │ │ │ │ +│ └───────────────────┴─────────────────────┘ │ +│ │ 1:N │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ checkin_record │ │ +│ │ (签到记录) │ │ +│ └──────────────────┘ │ │ │ │ ┌──────────────┐ │ │ │ member │ │ @@ -156,7 +156,7 @@ CREATE TABLE checkin_record ( created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), deleted_at TIMESTAMP DEFAULT NULL, - + CONSTRAINT fk_checkin_member FOREIGN KEY (member_id) REFERENCES member(id), CONSTRAINT fk_checkin_booking FOREIGN KEY (booking_id) REFERENCES booking_record(id) ); @@ -187,7 +187,7 @@ CREATE TABLE member_face ( created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), deleted_at TIMESTAMP DEFAULT NULL, - + CONSTRAINT fk_face_member FOREIGN KEY (member_id) REFERENCES member(id), CONSTRAINT uk_face_member UNIQUE (member_id) ); @@ -216,7 +216,7 @@ CREATE TABLE checkin_device ( created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), deleted_at TIMESTAMP DEFAULT NULL, - + CONSTRAINT fk_device_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id), CONSTRAINT fk_device_store FOREIGN KEY (store_id) REFERENCES store(id), CONSTRAINT uk_device_code UNIQUE (tenant_id, code) @@ -249,7 +249,7 @@ CREATE TABLE checkin_statistics ( created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), deleted_at TIMESTAMP DEFAULT NULL, - + CONSTRAINT uk_stat_date UNIQUE (tenant_id, store_id, stat_date, stat_type) ); @@ -283,7 +283,7 @@ CREATE TABLE checkin_rule ( created_by BIGINT, updated_by BIGINT, deleted_at TIMESTAMP DEFAULT NULL, - + CONSTRAINT fk_rule_tenant FOREIGN KEY (tenant_id) REFERENCES tenant(id) ); @@ -303,48 +303,48 @@ CREATE INDEX idx_rule_type ON checkin_rule(rule_type) WHERE deleted_at IS NULL; │ 签到聚合设计 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ CheckinRecord (聚合根) │ │ -│ ├───────────────────────────────────────────────────────────────────┤ │ -│ │ - id: Long │ │ -│ │ - tenantId: Long │ │ -│ │ - storeId: Long │ │ -│ │ - memberId: Long │ │ -│ │ - bookingId: Long? │ │ -│ │ - type: CheckinType │ │ -│ │ - method: CheckinMethod │ │ -│ │ - device: DeviceInfo? │ │ -│ │ - operator: OperatorInfo? │ │ -│ │ - status: CheckinStatus │ │ -│ │ - checkinAt: LocalDateTime │ │ -│ │ - benefit: BenefitDeduction? │ │ -│ │ │ │ -│ │ 行为: │ │ -│ │ + checkin(): void │ │ -│ │ + cancel(reason: String): void │ │ -│ │ + isLate(): Boolean │ │ -│ │ + getDuration(): Duration │ │ -│ └───────────────────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ CheckinRecord (聚合根) │ │ +│ ├───────────────────────────────────────────────────────────────────┤ │ +│ │ - id: Long │ │ +│ │ - tenantId: Long │ │ +│ │ - storeId: Long │ │ +│ │ - memberId: Long │ │ +│ │ - bookingId: Long? │ │ +│ │ - type: CheckinType │ │ +│ │ - method: CheckinMethod │ │ +│ │ - device: DeviceInfo? │ │ +│ │ - operator: OperatorInfo? │ │ +│ │ - status: CheckinStatus │ │ +│ │ - checkinAt: LocalDateTime │ │ +│ │ - benefit: BenefitDeduction? │ │ +│ │ │ │ +│ │ 行为: │ │ +│ │ + checkin(): void │ │ +│ │ + cancel(reason: String): void │ │ +│ │ + isLate(): Boolean │ │ +│ │ + getDuration(): Duration │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌───────────────────────┐ ┌───────────────────────┐ │ -│ │ CheckinGateway │ │ CheckinValidator │ │ -│ │ (签到网关) │ │ (签到验证器) │ │ -│ ├───────────────────────┤ ├───────────────────────┤ │ -│ │ + processQRCode() │ │ + validateMember() │ │ -│ │ + processFace() │ │ + validateBooking() │ │ -│ │ + processNFC() │ │ + validateBenefit() │ │ -│ │ + processManual() │ │ + validateRule() │ │ -│ └───────────────────────┘ └───────────────────────┘ │ +│ ┌───────────────────────┐ ┌───────────────────────┐ │ +│ │ CheckinGateway │ │ CheckinValidator │ │ +│ │ (签到网关) │ │ (签到验证器) │ │ +│ ├───────────────────────┤ ├───────────────────────┤ │ +│ │ + processQRCode() │ │ + validateMember() │ │ +│ │ + processFace() │ │ + validateBooking() │ │ +│ │ + processNFC() │ │ + validateBenefit() │ │ +│ │ + processManual() │ │ + validateRule() │ │ +│ └───────────────────────┘ └───────────────────────┘ │ │ │ -│ ┌───────────────────────┐ ┌───────────────────────┐ │ -│ │ CheckinStatistics │ │ FaceRecognition │ │ -│ │ (签到统计) │ │ (人脸识别) │ │ -│ ├───────────────────────┤ ├───────────────────────┤ │ -│ │ + dailyStats() │ │ + register() │ │ -│ │ + weeklyStats() │ │ + match() │ │ -│ │ + monthlyStats() │ │ + update() │ │ -│ │ + memberStats() │ │ + delete() │ │ -│ └───────────────────────┘ └───────────────────────┘ │ +│ ┌───────────────────────┐ ┌───────────────────────┐ │ +│ │ CheckinStatistics │ │ FaceRecognition │ │ +│ │ (签到统计) │ │ (人脸识别) │ │ +│ ├───────────────────────┤ ├───────────────────────┤ │ +│ │ + dailyStats() │ │ + register() │ │ +│ │ + weeklyStats() │ │ + match() │ │ +│ │ + monthlyStats() │ │ + update() │ │ +│ │ + memberStats() │ │ + delete() │ │ +│ └───────────────────────┘ └───────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -357,7 +357,7 @@ public enum CheckinType { COURSE(2, "课程签到"), PRIVATE(3, "私教签到"), ACTIVITY(4, "活动签到"); - + private final int code; private final String desc; } @@ -367,7 +367,7 @@ public enum CheckinMethod { FACE(2, "人脸识别"), NFC(3, "NFC"), MANUAL(4, "教练代签"); - + private final int code; private final String desc; } @@ -376,7 +376,7 @@ public enum CheckinStatus { SUCCESS(1, "成功"), FAILED(2, "失败"), CANCELLED(3, "已取消"); - + private final int code; private final String desc; } @@ -411,35 +411,35 @@ public record CheckinResult( ```java public interface CheckinDomainService { - + Mono processCheckin(CheckinRequest request); - + Mono cancelCheckin(Long checkinId, String reason); - + Mono validateCheckinEligibility(Long memberId, CheckinType type, Long bookingId); - + Mono getCheckinRecord(Long checkinId); - + Flux getMemberCheckinHistory(Long memberId, LocalDate startDate, LocalDate endDate); } public interface FaceRecognitionService { - + Mono registerFace(Long memberId, byte[] faceImage); - + Mono matchFace(byte[] faceFeature, Long tenantId); - + Mono updateFace(Long memberId, byte[] faceImage); - + Mono deleteFace(Long memberId); } public interface CheckinStatisticsService { - + Mono generateDailyStatistics(Long tenantId, Long storeId, LocalDate date); - + Mono getDailyStatistics(Long tenantId, Long storeId, LocalDate date); - + Mono> getMemberCheckinStats(Long memberId, LocalDate startDate, LocalDate endDate); } ``` @@ -455,47 +455,47 @@ public interface CheckinStatisticsService { │ 入场签到流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ -│ │ 会员 │ │ 签到 │ │ 签到 │ │ 权益 │ │ 签到 │ │ -│ │ │ │ 网关 │ │ 验证 │ │ 服务 │ │ 记录 │ │ -│ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ │ -│ │ │ │ │ │ │ -│ │ 出示二维码 │ │ │ │ │ -│ │────────────▶│ │ │ │ │ -│ │ │ │ │ │ │ -│ │ │ 解析二维码 │ │ │ │ -│ │ │────────────▶│ │ │ │ -│ │ │ │ │ │ │ -│ │ │ │ 查询会员 │ │ │ -│ │ │ │────────────▶│ │ │ -│ │ │ │ │ │ │ -│ │ │ │ 会员信息 │ │ │ -│ │ │ │◀────────────│ │ │ -│ │ │ │ │ │ │ -│ │ │ │ 验证会员卡 │ │ │ -│ │ │ │────────────▶│ │ │ -│ │ │ │ │ │ │ -│ │ │ │ 权益状态 │ │ │ -│ │ │ │◀────────────│ │ │ -│ │ │ │ │ │ │ -│ │ │ │ 检查签到规则│ │ │ -│ │ │ │─────────────┼────────────▶│ │ -│ │ │ │ │ │ │ -│ │ │ │ 规则验证结果│ │ │ -│ │ │ │◀────────────┼─────────────│ │ -│ │ │ │ │ │ │ -│ │ │ 验证结果 │ │ │ │ -│ │ │◀────────────│ │ │ │ -│ │ │ │ │ │ │ -│ │ │ 创建签到记录│ │ │ │ -│ │ │─────────────┼─────────────┼────────────▶│ │ -│ │ │ │ │ │ │ -│ │ │ 签到成功 │ │ │ │ -│ │ │◀────────────┼─────────────┼─────────────│ │ -│ │ │ │ │ │ │ -│ │ 签到成功 │ │ │ │ │ -│ │◀────────────│ │ │ │ │ -│ │ │ │ │ │ │ +│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ +│ │ 会员 │ │ 签到 │ │ 签到 │ │ 权益 │ │ 签到 │ │ +│ │ │ │ 网关 │ │ 验证 │ │ 服务 │ │ 记录 │ │ +│ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ │ +│ │ │ │ │ │ │ +│ │ 出示二维码 │ │ │ │ │ +│ │────────────▶│ │ │ │ │ +│ │ │ │ │ │ │ +│ │ │ 解析二维码 │ │ │ │ +│ │ │────────────▶│ │ │ │ +│ │ │ │ │ │ │ +│ │ │ │ 查询会员 │ │ │ +│ │ │ │────────────▶│ │ │ +│ │ │ │ │ │ │ +│ │ │ │ 会员信息 │ │ │ +│ │ │ │◀────────────│ │ │ +│ │ │ │ │ │ │ +│ │ │ │ 验证会员卡 │ │ │ +│ │ │ │────────────▶│ │ │ +│ │ │ │ │ │ │ +│ │ │ │ 权益状态 │ │ │ +│ │ │ │◀────────────│ │ │ +│ │ │ │ │ │ │ +│ │ │ │ 检查签到规则│ │ │ +│ │ │ │─────────────┼────────────▶│ │ +│ │ │ │ │ │ │ +│ │ │ │ 规则验证结果│ │ │ +│ │ │ │◀────────────┼─────────────│ │ +│ │ │ │ │ │ │ +│ │ │ 验证结果 │ │ │ │ +│ │ │◀────────────│ │ │ │ +│ │ │ │ │ │ │ +│ │ │ 创建签到记录│ │ │ │ +│ │ │─────────────┼─────────────┼────────────▶│ │ +│ │ │ │ │ │ │ +│ │ │ 签到成功 │ │ │ │ +│ │ │◀────────────┼─────────────┼─────────────│ │ +│ │ │ │ │ │ │ +│ │ 签到成功 │ │ │ │ │ +│ │◀────────────│ │ │ │ │ +│ │ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -507,47 +507,47 @@ public interface CheckinStatisticsService { │ 课程签到流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ -│ │ 会员 │ │ 签到 │ │ 预约 │ │ 权益 │ │ 签到 │ │ -│ │ │ │ 网关 │ │ 服务 │ │ 服务 │ │ 记录 │ │ -│ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ │ -│ │ │ │ │ │ │ -│ │ 扫码签到 │ │ │ │ │ -│ │────────────▶│ │ │ │ │ -│ │ │ │ │ │ │ -│ │ │ 查询预约 │ │ │ │ -│ │ │────────────▶│ │ │ │ -│ │ │ │ │ │ │ -│ │ │ 预约信息 │ │ │ │ -│ │ │◀────────────│ │ │ │ -│ │ │ │ │ │ │ -│ │ │ 验证签到时间窗口 │ │ │ -│ │ │─────────────┼────────────▶│ │ │ -│ │ │ │ │ │ │ -│ │ │ 时间窗口验证结果 │ │ │ -│ │ │◀────────────┼─────────────│ │ │ -│ │ │ │ │ │ │ -│ │ │ 验证权益 │ │ │ │ -│ │ │─────────────┼────────────▶│ │ │ -│ │ │ │ │ │ │ -│ │ │ 权益状态 │ │ │ │ -│ │ │◀────────────┼─────────────│ │ │ -│ │ │ │ │ │ │ -│ │ │ 扣减权益 │ │ │ │ -│ │ │─────────────┼────────────▶│ │ │ -│ │ │ │ │ │ │ -│ │ │ 扣减结果 │ │ │ │ -│ │ │◀────────────┼─────────────│ │ │ -│ │ │ │ │ │ │ -│ │ │ 创建签到记录│ │ │ │ -│ │ │─────────────┼─────────────┼────────────▶│ │ -│ │ │ │ │ │ │ -│ │ │ 更新预约签到状态 │ │ │ -│ │ │────────────▶│ │ │ │ -│ │ │ │ │ │ │ -│ │ 签到成功 │ │ │ │ │ -│ │◀────────────│ │ │ │ │ -│ │ │ │ │ │ │ +│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ +│ │ 会员 │ │ 签到 │ │ 预约 │ │ 权益 │ │ 签到 │ │ +│ │ │ │ 网关 │ │ 服务 │ │ 服务 │ │ 记录 │ │ +│ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ │ +│ │ │ │ │ │ │ +│ │ 扫码签到 │ │ │ │ │ +│ │────────────▶│ │ │ │ │ +│ │ │ │ │ │ │ +│ │ │ 查询预约 │ │ │ │ +│ │ │────────────▶│ │ │ │ +│ │ │ │ │ │ │ +│ │ │ 预约信息 │ │ │ │ +│ │ │◀────────────│ │ │ │ +│ │ │ │ │ │ │ +│ │ │ 验证签到时间窗口 │ │ │ +│ │ │─────────────┼────────────▶│ │ │ +│ │ │ │ │ │ │ +│ │ │ 时间窗口验证结果 │ │ │ +│ │ │◀────────────┼─────────────│ │ │ +│ │ │ │ │ │ │ +│ │ │ 验证权益 │ │ │ │ +│ │ │─────────────┼────────────▶│ │ │ +│ │ │ │ │ │ │ +│ │ │ 权益状态 │ │ │ │ +│ │ │◀────────────┼─────────────│ │ │ +│ │ │ │ │ │ │ +│ │ │ 扣减权益 │ │ │ │ +│ │ │─────────────┼────────────▶│ │ │ +│ │ │ │ │ │ │ +│ │ │ 扣减结果 │ │ │ │ +│ │ │◀────────────┼─────────────│ │ │ +│ │ │ │ │ │ │ +│ │ │ 创建签到记录│ │ │ │ +│ │ │─────────────┼─────────────┼────────────▶│ │ +│ │ │ │ │ │ │ +│ │ │ 更新预约签到状态 │ │ │ +│ │ │────────────▶│ │ │ │ +│ │ │ │ │ │ │ +│ │ 签到成功 │ │ │ │ │ +│ │◀────────────│ │ │ │ │ +│ │ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -559,35 +559,35 @@ public interface CheckinStatisticsService { │ 人脸识别签到流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ -│ │ 会员 │ │ 人脸 │ │ 人脸 │ │ 签到 │ │ 签到 │ │ -│ │ │ │ 设备 │ │ 服务 │ │ 验证 │ │ 记录 │ │ -│ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ │ -│ │ │ │ │ │ │ -│ │ 人脸识别 │ │ │ │ │ -│ │────────────▶│ │ │ │ │ -│ │ │ │ │ │ │ -│ │ │ 提取特征值 │ │ │ │ -│ │ │────────────▶│ │ │ │ -│ │ │ │ │ │ │ -│ │ │ │ 匹配会员 │ │ │ -│ │ │ │─────────────┼────────────▶│ │ -│ │ │ │ │ │ │ -│ │ │ │ 匹配结果 │ │ │ -│ │ │ │◀────────────┼─────────────│ │ -│ │ │ │ │ │ │ -│ │ │ 会员ID │ │ │ │ -│ │ │◀────────────│ │ │ │ -│ │ │ │ │ │ │ -│ │ │ 执行签到流程│ │ │ │ -│ │ │─────────────┼────────────▶│ │ │ -│ │ │ │ │ │ │ -│ │ │ 签到结果 │ │ │ │ -│ │ │◀────────────┼─────────────┼─────────────│ │ -│ │ │ │ │ │ │ -│ │ 签到成功 │ │ │ │ │ -│ │◀────────────│ │ │ │ │ -│ │ │ │ │ │ │ +│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ +│ │ 会员 │ │ 人脸 │ │ 人脸 │ │ 签到 │ │ 签到 │ │ +│ │ │ │ 设备 │ │ 服务 │ │ 验证 │ │ 记录 │ │ +│ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ │ +│ │ │ │ │ │ │ +│ │ 人脸识别 │ │ │ │ │ +│ │────────────▶│ │ │ │ │ +│ │ │ │ │ │ │ +│ │ │ 提取特征值 │ │ │ │ +│ │ │────────────▶│ │ │ │ +│ │ │ │ │ │ │ +│ │ │ │ 匹配会员 │ │ │ +│ │ │ │─────────────┼────────────▶│ │ +│ │ │ │ │ │ │ +│ │ │ │ 匹配结果 │ │ │ +│ │ │ │◀────────────┼─────────────│ │ +│ │ │ │ │ │ │ +│ │ │ 会员ID │ │ │ │ +│ │ │◀────────────│ │ │ │ +│ │ │ │ │ │ │ +│ │ │ 执行签到流程│ │ │ │ +│ │ │─────────────┼────────────▶│ │ │ +│ │ │ │ │ │ │ +│ │ │ 签到结果 │ │ │ │ +│ │ │◀────────────┼─────────────┼─────────────│ │ +│ │ │ │ │ │ │ +│ │ 签到成功 │ │ │ │ │ +│ │◀────────────│ │ │ │ │ +│ │ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -599,41 +599,41 @@ public interface CheckinStatisticsService { │ 教练代签流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ -│ │ 教练 │ │ 签到 │ │ 预约 │ │ 权益 │ │ 签到 │ │ -│ │ │ │ 网关 │ │ 服务 │ │ 服务 │ │ 记录 │ │ -│ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ │ -│ │ │ │ │ │ │ -│ │ 选择会员 │ │ │ │ │ -│ │────────────▶│ │ │ │ │ -│ │ │ │ │ │ │ -│ │ │ 验证教练身份│ │ │ │ -│ │ │─────────────┼─────────────┼────────────▶│ │ -│ │ │ │ │ │ │ -│ │ │ 身份验证结果│ │ │ │ -│ │ │◀────────────┼─────────────┼─────────────│ │ -│ │ │ │ │ │ │ -│ │ │ 查询会员预约│ │ │ │ -│ │ │────────────▶│ │ │ │ -│ │ │ │ │ │ │ -│ │ │ 预约列表 │ │ │ │ -│ │ │◀────────────│ │ │ │ -│ │ │ │ │ │ │ -│ │ 选择预约 │ │ │ │ │ -│ │────────────▶│ │ │ │ │ -│ │ │ │ │ │ │ -│ │ │ 验证签到资格│ │ │ │ -│ │ │─────────────┼────────────▶│ │ │ -│ │ │ │ │ │ │ -│ │ │ 扣减权益 │ │ │ │ -│ │ │─────────────┼────────────▶│ │ │ -│ │ │ │ │ │ │ -│ │ │ 创建签到记录│ │ │ │ -│ │ │─────────────┼─────────────┼────────────▶│ │ -│ │ │ │ │ │ │ -│ │ 代签成功 │ │ │ │ │ -│ │◀────────────│ │ │ │ │ -│ │ │ │ │ │ │ +│ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ +│ │ 教练 │ │ 签到 │ │ 预约 │ │ 权益 │ │ 签到 │ │ +│ │ │ │ 网关 │ │ 服务 │ │ 服务 │ │ 记录 │ │ +│ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ └───┬───┘ │ +│ │ │ │ │ │ │ +│ │ 选择会员 │ │ │ │ │ +│ │────────────▶│ │ │ │ │ +│ │ │ │ │ │ │ +│ │ │ 验证教练身份│ │ │ │ +│ │ │─────────────┼─────────────┼────────────▶│ │ +│ │ │ │ │ │ │ +│ │ │ 身份验证结果│ │ │ │ +│ │ │◀────────────┼─────────────┼─────────────│ │ +│ │ │ │ │ │ │ +│ │ │ 查询会员预约│ │ │ │ +│ │ │────────────▶│ │ │ │ +│ │ │ │ │ │ │ +│ │ │ 预约列表 │ │ │ │ +│ │ │◀────────────│ │ │ │ +│ │ │ │ │ │ │ +│ │ 选择预约 │ │ │ │ │ +│ │────────────▶│ │ │ │ │ +│ │ │ │ │ │ │ +│ │ │ 验证签到资格│ │ │ │ +│ │ │─────────────┼────────────▶│ │ │ +│ │ │ │ │ │ │ +│ │ │ 扣减权益 │ │ │ │ +│ │ │─────────────┼────────────▶│ │ │ +│ │ │ │ │ │ │ +│ │ │ 创建签到记录│ │ │ │ +│ │ │─────────────┼─────────────┼────────────▶│ │ +│ │ │ │ │ │ │ +│ │ 代签成功 │ │ │ │ │ +│ │◀────────────│ │ │ │ │ +│ │ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -969,7 +969,7 @@ Response: @Service @RequiredArgsConstructor public class CheckinDomainServiceImpl implements CheckinDomainService { - + private final CheckinRecordRepository checkinRepository; private final MemberRepository memberRepository; private final BookingRecordRepository bookingRepository; @@ -977,16 +977,16 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { private final CheckinRuleRepository ruleRepository; private final TransactionalOperator rxtx; private final ApplicationEventPublisher eventPublisher; - + @Override public Mono processCheckin(CheckinRequest request) { - return Mono.defer(() -> + return Mono.defer(() -> validateMember(request.getTenantId(), request.getMemberId()) .flatMap(member -> validateCheckinRule(member, request)) .flatMap(member -> processCheckinByType(member, request)) ).as(rxtx::transactional); } - + private Mono validateMember(Long tenantId, Long memberId) { return memberRepository.findByIdAndTenantId(memberId, tenantId) .switchIfEmpty(Mono.error(new CheckinException(CheckinException.MEMBER_NOT_FOUND))) @@ -997,10 +997,10 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { return Mono.just(member); }); } - + private Mono validateCheckinRule(Member member, CheckinRequest request) { return ruleRepository.findByTenantIdAndRuleType( - member.getTenantId(), + member.getTenantId(), request.getType() ) .flatMap(rule -> { @@ -1011,11 +1011,11 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { }) .switchIfEmpty(Mono.just(member)); } - + private Mono validateEntryRule(Member member, CheckinRule rule, CheckinRequest request) { LocalDateTime todayStart = LocalDate.now().atStartOfDay(); LocalDateTime todayEnd = todayStart.plusDays(1); - + return checkinRepository.countByMemberIdAndTypeAndCheckinAtBetween( member.getId(), CheckinType.ENTRY, @@ -1028,23 +1028,23 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { "今日入场次数已达上限" )); } - + if (rule.getIntervalMinutes() > 0) { return validateCheckinInterval(member, rule); } - + return Mono.just(member); }); } - + private Mono validateCheckinInterval(Member member, CheckinRule rule) { return checkinRepository.findFirstByMemberIdOrderByCheckinAtDesc(member.getId()) .flatMap(lastCheckin -> { long minutes = Duration.between( - lastCheckin.getCheckinAt(), + lastCheckin.getCheckinAt(), LocalDateTime.now() ).toMinutes(); - + if (minutes < rule.getIntervalMinutes()) { return Mono.error(new CheckinException( CheckinException.INTERVAL_NOT_MET, @@ -1055,7 +1055,7 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { }) .switchIfEmpty(Mono.just(member)); } - + private Mono processCheckinByType(Member member, CheckinRequest request) { return switch (request.getType()) { case ENTRY -> processEntryCheckin(member, request); @@ -1064,7 +1064,7 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { case ACTIVITY -> processActivityCheckin(member, request); }; } - + private Mono processEntryCheckin(Member member, CheckinRequest request) { return benefitService.validateAndDeduct( member.getId(), @@ -1074,7 +1074,7 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { ).flatMap(benefitDeduction -> { CheckinRecord record = buildCheckinRecord(member, request, benefitDeduction); record.setType(CheckinType.ENTRY); - + return checkinRepository.save(record) .doOnNext(saved -> eventPublisher.publishEvent( new CheckinSuccessEvent(saved) @@ -1087,7 +1087,7 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { return Mono.error(e); }); } - + private Mono processCourseCheckin(Member member, CheckinRequest request) { return bookingRepository.findById(request.getBookingId()) .switchIfEmpty(Mono.error(new CheckinException(CheckinException.BOOKING_NOT_FOUND))) @@ -1096,16 +1096,16 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { LocalDateTime now = LocalDateTime.now(); LocalDateTime courseStart = booking.getSlot().getStartTime(); long minutesBefore = Duration.between(now, courseStart).toMinutes(); - + CheckinRecord record = buildCheckinRecord(member, request, null); record.setType(CheckinType.COURSE); record.setBookingId(booking.getId()); - + if (minutesBefore < 0) { record.setLate(true); record.setLateMinutes((int) Math.abs(minutesBefore)); } - + return checkinRepository.save(record) .flatMap(saved -> updateBookingCheckinStatus(booking, saved)) .doOnNext(saved -> eventPublisher.publishEvent( @@ -1114,40 +1114,40 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { .map(saved -> CheckinResult.success(saved)); }); } - + private Mono validateBookingForCheckin(BookingRecord booking, Member member) { if (!booking.getMemberId().equals(member.getId())) { return Mono.error(new CheckinException(CheckinException.BOOKING_NOT_MATCH)); } - + if (booking.getStatus() != BookingStatus.CONFIRMED) { return Mono.error(new CheckinException(CheckinException.BOOKING_NOT_CONFIRMED)); } - + if (booking.getCheckinStatus() == CheckinStatus.CHECKED) { return Mono.error(new CheckinException(CheckinException.ALREADY_CHECKED)); } - + LocalDateTime now = LocalDateTime.now(); LocalDateTime courseStart = booking.getSlot().getStartTime(); LocalDateTime courseEnd = booking.getSlot().getEndTime(); - + if (now.isAfter(courseEnd)) { return Mono.error(new CheckinException(CheckinException.COURSE_ENDED)); } - + return Mono.just(booking); } - + private Mono updateBookingCheckinStatus(BookingRecord booking, CheckinRecord checkin) { booking.setCheckinStatus(checkin.isLate() ? CheckinStatus.LATE : CheckinStatus.CHECKED); booking.setCheckinAt(checkin.getCheckinAt()); booking.setCheckinBy(checkin.getOperatorId()); - + return bookingRepository.save(booking).thenReturn(checkin); } - - private CheckinRecord buildCheckinRecord(Member member, CheckinRequest request, + + private CheckinRecord buildCheckinRecord(Member member, CheckinRequest request, BenefitDeduction deduction) { CheckinRecord record = new CheckinRecord(); record.setTenantId(member.getTenantId()); @@ -1160,16 +1160,16 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { record.setStatus(CheckinStatus.SUCCESS); record.setCheckinAt(LocalDateTime.now()); record.setCheckinDate(LocalDate.now()); - + if (deduction != null) { record.setBenefitId(deduction.benefitId()); record.setBenefitType(deduction.benefitType()); record.setBenefitValue(deduction.benefitValue()); } - + return record; } - + @Override public Mono cancelCheckin(Long checkinId, String reason) { return checkinRepository.findById(checkinId) @@ -1178,10 +1178,10 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { if (record.getStatus() == CheckinStatus.CANCELLED) { return Mono.error(new CheckinException(CheckinException.ALREADY_CANCELLED)); } - + record.setStatus(CheckinStatus.CANCELLED); record.setFailReason(reason); - + return checkinRepository.save(record) .flatMap(saved -> { if (saved.getBenefitId() != null) { @@ -1196,7 +1196,7 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { }) .then(); } - + @Override public Mono validateCheckinEligibility(Long memberId, CheckinType type, Long bookingId) { return memberRepository.findById(memberId) @@ -1204,17 +1204,17 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { if (member.getStatus() != MemberStatus.ACTIVE) { return Mono.just(false); } - + if (type == CheckinType.ENTRY) { return benefitService.hasValidBenefit(memberId, BenefitType.TIME); } - + if (bookingId != null) { return bookingRepository.findById(bookingId) - .map(booking -> booking.getStatus() == BookingStatus.CONFIRMED + .map(booking -> booking.getStatus() == BookingStatus.CONFIRMED && booking.getCheckinStatus() == CheckinStatus.NOT_CHECKED); } - + return Mono.just(true); }) .switchIfEmpty(Mono.just(false)); @@ -1229,16 +1229,16 @@ public class CheckinDomainServiceImpl implements CheckinDomainService { @Service @RequiredArgsConstructor public class FaceRecognitionServiceImpl implements FaceRecognitionService { - + private final MemberFaceRepository faceRepository; private final MemberRepository memberRepository; private final FaceFeatureExtractor featureExtractor; private final Cache faceFeatureCache; private final TransactionalOperator rxtx; - + private static final float MATCH_THRESHOLD = 0.85f; private static final float QUALITY_THRESHOLD = 60.0f; - + @Override public Mono registerFace(Long memberId, byte[] faceImage) { return Mono.fromCallable(() -> featureExtractor.extractFeature(faceImage)) @@ -1250,7 +1250,7 @@ public class FaceRecognitionServiceImpl implements FaceRecognitionService { "人脸质量分数过低: " + featureResult.qualityScore() )); } - + return faceRepository.existsByMemberId(memberId) .flatMap(exists -> { if (exists) { @@ -1258,14 +1258,14 @@ public class FaceRecognitionServiceImpl implements FaceRecognitionService { FaceException.FACE_ALREADY_REGISTERED )); } - + MemberFace face = new MemberFace(); face.setMemberId(memberId); face.setFaceFeature(featureResult.feature()); face.setQualityScore(featureResult.qualityScore()); face.setFeatureVersion("v1.0"); face.setStatus(FaceStatus.ACTIVE); - + return faceRepository.save(face) .doOnNext(saved -> faceFeatureCache.put(memberId, saved.getFaceFeature())) .thenReturn(true); @@ -1273,30 +1273,30 @@ public class FaceRecognitionServiceImpl implements FaceRecognitionService { }) .as(rxtx::transactional); } - + @Override public Mono matchFace(byte[] faceFeature, Long tenantId) { return Mono.fromCallable(() -> { List faces = faceRepository.findAllByTenantIdAndStatus( - tenantId, + tenantId, FaceStatus.ACTIVE ); - + float maxSimilarity = 0; Long matchedMemberId = null; - + for (MemberFace face : faces) { byte[] cachedFeature = faceFeatureCache.getIfPresent(face.getMemberId()); byte[] targetFeature = cachedFeature != null ? cachedFeature : face.getFaceFeature(); - + float similarity = featureExtractor.compareFeature(faceFeature, targetFeature); - + if (similarity > maxSimilarity && similarity >= MATCH_THRESHOLD) { maxSimilarity = similarity; matchedMemberId = face.getMemberId(); } } - + return matchedMemberId; }) .subscribeOn(Schedulers.boundedElastic()) @@ -1304,12 +1304,12 @@ public class FaceRecognitionServiceImpl implements FaceRecognitionService { if (memberId == null) { return Mono.error(new FaceException(FaceException.FACE_NOT_MATCHED)); } - + return faceRepository.updateMatchInfo(memberId, LocalDateTime.now()) .thenReturn(memberId); }); } - + @Override public Mono updateFace(Long memberId, byte[] faceImage) { return Mono.fromCallable(() -> featureExtractor.extractFeature(faceImage)) @@ -1321,14 +1321,14 @@ public class FaceRecognitionServiceImpl implements FaceRecognitionService { "人脸质量分数过低: " + featureResult.qualityScore() )); } - + return faceRepository.findByMemberId(memberId) .switchIfEmpty(Mono.error(new FaceException(FaceException.FACE_NOT_FOUND))) .flatMap(face -> { face.setFaceFeature(featureResult.feature()); face.setQualityScore(featureResult.qualityScore()); face.setStatus(FaceStatus.ACTIVE); - + return faceRepository.save(face) .doOnNext(saved -> faceFeatureCache.put(memberId, saved.getFaceFeature())) .thenReturn(true); @@ -1336,7 +1336,7 @@ public class FaceRecognitionServiceImpl implements FaceRecognitionService { }) .as(rxtx::transactional); } - + @Override public Mono deleteFace(Long memberId) { return faceRepository.deleteByMemberId(memberId) @@ -1353,20 +1353,20 @@ public class FaceRecognitionServiceImpl implements FaceRecognitionService { @Service @RequiredArgsConstructor public class CheckinGateway { - + private final CheckinDomainService checkinService; private final MemberRepository memberRepository; private final QRCodeValidator qrCodeValidator; private final NFCService nfcService; - + public Mono processQRCode(CheckinQRCodeRequest request) { return Mono.defer(() -> { QRCodeInfo qrInfo = qrCodeValidator.parseAndValidate(request.getQrcode()); - + if (qrInfo.isExpired()) { return Mono.just(CheckinResult.failure("二维码已过期,请刷新")); } - + CheckinRequest checkinRequest = new CheckinRequest(); checkinRequest.setTenantId(request.getTenantId()); checkinRequest.setStoreId(request.getStoreId()); @@ -1375,11 +1375,11 @@ public class CheckinGateway { checkinRequest.setDeviceId(request.getDeviceId()); checkinRequest.setType(CheckinType.fromCode(request.getType())); checkinRequest.setBookingId(request.getBookingId()); - + return checkinService.processCheckin(checkinRequest); }); } - + public Mono processNFC(CheckinNFCRequest request) { return nfcService.getMemberByNFC(request.getNfcId()) .flatMap(member -> { @@ -1391,7 +1391,7 @@ public class CheckinGateway { checkinRequest.setDeviceId(request.getDeviceId()); checkinRequest.setType(CheckinType.fromCode(request.getType())); checkinRequest.setBookingId(request.getBookingId()); - + return checkinService.processCheckin(checkinRequest); }) .onErrorResume(e -> { @@ -1401,7 +1401,7 @@ public class CheckinGateway { return Mono.error(e); }); } - + public Mono processManual(CheckinManualRequest request) { return Mono.defer(() -> { CheckinRequest checkinRequest = new CheckinRequest(); @@ -1413,7 +1413,7 @@ public class CheckinGateway { checkinRequest.setBookingId(request.getBookingId()); checkinRequest.setOperatorId(request.getOperatorId()); checkinRequest.setOperatorName(request.getOperatorName()); - + return checkinService.processCheckin(checkinRequest); }); } @@ -1427,46 +1427,46 @@ public class CheckinGateway { @Service @RequiredArgsConstructor public class CheckinStatisticsServiceImpl implements CheckinStatisticsService { - + private final CheckinRecordRepository checkinRepository; private final CheckinStatisticsRepository statisticsRepository; private final MemberRepository memberRepository; - + @Override public Mono generateDailyStatistics(Long tenantId, Long storeId, LocalDate date) { return Mono.defer(() -> { LocalDateTime startOfDay = date.atStartOfDay(); LocalDateTime endOfDay = startOfDay.plusDays(1); - + Mono totalCount = checkinRepository.countByTenantIdAndStoreIdAndCheckinAtBetween( tenantId, storeId, startOfDay, endOfDay ); - + Mono entryCount = checkinRepository.countByTenantIdAndStoreIdAndTypeAndCheckinAtBetween( tenantId, storeId, CheckinType.ENTRY, startOfDay, endOfDay ); - + Mono courseCount = checkinRepository.countByTenantIdAndStoreIdAndTypeAndCheckinAtBetween( tenantId, storeId, CheckinType.COURSE, startOfDay, endOfDay ); - + Mono privateCount = checkinRepository.countByTenantIdAndStoreIdAndTypeAndCheckinAtBetween( tenantId, storeId, CheckinType.PRIVATE, startOfDay, endOfDay ); - + Mono activityCount = checkinRepository.countByTenantIdAndStoreIdAndTypeAndCheckinAtBetween( tenantId, storeId, CheckinType.ACTIVITY, startOfDay, endOfDay ); - + Mono activeMemberCount = checkinRepository.countDistinctMemberByTenantIdAndStoreIdAndCheckinAtBetween( tenantId, storeId, startOfDay, endOfDay ); - + Mono> hourlyDistribution = checkinRepository .findHourlyDistribution(tenantId, storeId, startOfDay, endOfDay) .collectMap(CheckinHourlyStats::getHour, CheckinHourlyStats::getCount); - - return Mono.zip(totalCount, entryCount, courseCount, privateCount, + + return Mono.zip(totalCount, entryCount, courseCount, privateCount, activityCount, activeMemberCount, hourlyDistribution) .flatMap(tuple -> { CheckinStatistics stats = new CheckinStatistics(); @@ -1480,7 +1480,7 @@ public class CheckinStatisticsServiceImpl implements CheckinStatisticsService { stats.setPrivateCount(tuple.getT4().intValue()); stats.setActivityCount(tuple.getT5().intValue()); stats.setActiveMemberCount(tuple.getT6()); - + Map hourly = tuple.getT7(); if (!hourly.isEmpty()) { stats.setPeakHour( @@ -1496,38 +1496,38 @@ public class CheckinStatisticsServiceImpl implements CheckinStatisticsService { .orElse(0) ); } - + return statisticsRepository.save(stats); }) .then(); }); } - + @Override public Mono getDailyStatistics(Long tenantId, Long storeId, LocalDate date) { return statisticsRepository.findByTenantIdAndStoreIdAndStatDateAndStatType( tenantId, storeId, date, StatType.DAILY ); } - + @Override - public Mono> getMemberCheckinStats(Long memberId, + public Mono> getMemberCheckinStats(Long memberId, LocalDate startDate, LocalDate endDate) { LocalDateTime start = startDate.atStartOfDay(); LocalDateTime end = endDate.plusDays(1).atStartOfDay(); - + Mono totalCount = checkinRepository.countByMemberIdAndCheckinAtBetween( memberId, start, end ); - + Mono> typeDistribution = checkinRepository .countByMemberIdGroupByType(memberId, start, end) .collectMap(CheckinTypeStats::getType, CheckinTypeStats::getCount); - + Mono> checkinDates = checkinRepository .findDistinctCheckinDatesByMemberId(memberId, start, end) .collectList(); - + return Mono.zip(totalCount, typeDistribution, checkinDates) .map(tuple -> { Map result = new HashMap<>(); @@ -1547,12 +1547,12 @@ public class CheckinStatisticsServiceImpl implements CheckinStatisticsService { ### 7.1 签到并发场景分析 -| 场景 | 并发特点 | 处理策略 | -|------|---------|---------| -| 早高峰入场 | 短时间内大量签到请求 | 本地缓存+异步处理 | -| 课程签到窗口 | 集中签到时段 | 预加载+限流 | -| 人脸识别匹配 | 计算密集型 | 特征缓存+批量匹配 | -| 统计计算 | 数据量大 | 异步任务+增量计算 | +| 场景 | 并发特点 | 处理策略 | +| ------------ | -------------------- | ----------------- | +| 早高峰入场 | 短时间内大量签到请求 | 本地缓存+异步处理 | +| 课程签到窗口 | 集中签到时段 | 预加载+限流 | +| 人脸识别匹配 | 计算密集型 | 特征缓存+批量匹配 | +| 统计计算 | 数据量大 | 异步任务+增量计算 | ### 7.2 签到限流设计 @@ -1561,22 +1561,22 @@ public class CheckinStatisticsServiceImpl implements CheckinStatisticsService { @Component @RequiredArgsConstructor public class CheckinRateLimiter { - + private final Cache rateLimitCache = Caffeine.newBuilder() .expireAfterWrite(Duration.ofSeconds(1)) .build(); - + private static final int MAX_REQUESTS_PER_SECOND = 100; - + public Mono allowRequest(Long tenantId, Long storeId) { String key = tenantId + ":" + storeId + ":" + System.currentTimeMillis() / 1000; - + return Mono.fromCallable(() -> { AtomicInteger counter = rateLimitCache.get(key, k -> new AtomicInteger(0)); return counter.incrementAndGet() <= MAX_REQUESTS_PER_SECOND; }); } - + public Mono withRateLimit(Long tenantId, Long storeId, Mono action) { return allowRequest(tenantId, storeId) .flatMap(allowed -> { @@ -1598,19 +1598,19 @@ public class CheckinRateLimiter { @Slf4j @Component public class FaceFeatureCacheManager { - + private final Cache featureCache = Caffeine.newBuilder() .maximumSize(10000) .expireAfterAccess(Duration.ofHours(24)) .recordStats() .build(); - + private final MemberFaceRepository faceRepository; - + @Scheduled(fixedRate = 300000) public void preloadFeatures() { log.info("开始预加载人脸特征..."); - + faceRepository.findAllByStatus(FaceStatus.ACTIVE) .doOnNext(face -> featureCache.put(face.getMemberId(), face.getFaceFeature())) .then() @@ -1619,19 +1619,19 @@ public class FaceFeatureCacheManager { e -> log.error("人脸特征预加载失败", e) ); } - + public Optional getFeature(Long memberId) { return Optional.ofNullable(featureCache.getIfPresent(memberId)); } - + public void putFeature(Long memberId, byte[] feature) { featureCache.put(memberId, feature); } - + public void invalidate(Long memberId) { featureCache.invalidate(memberId); } - + public CacheStats getStats() { return featureCache.stats(); } @@ -1645,11 +1645,11 @@ public class FaceFeatureCacheManager { @Service @RequiredArgsConstructor public class AsyncCheckinProcessor { - + private final CheckinRecordRepository checkinRepository; private final ApplicationEventPublisher eventPublisher; private final Sinks.Many checkinSink; - + @PostConstruct public void init() { checkinSink.asFlux() @@ -1659,14 +1659,14 @@ public class AsyncCheckinProcessor { e -> log.error("异步签到处理错误", e) ); } - + public Mono submitAsync(CheckinTask task) { return Mono.fromCallable(() -> { checkinSink.tryEmitNext(task); return task.getTaskId(); }); } - + private Mono processAsync(CheckinTask task) { return processCheckin(task) .flatMap(record -> { @@ -1679,17 +1679,17 @@ public class AsyncCheckinProcessor { }) .then(); } - + private Mono processCheckin(CheckinTask task) { // 签到处理逻辑 } - + private Mono saveFailedRecord(CheckinTask task, Throwable e) { CheckinRecord record = new CheckinRecord(); record.setStatus(CheckinStatus.FAILED); record.setFailReason(e.getMessage()); // 设置其他字段... - + return checkinRepository.save(record).then(); } } @@ -1702,19 +1702,19 @@ public class AsyncCheckinProcessor { ### 8.1 缓存策略 | 数据类型 | 缓存位置 | 过期时间 | 更新策略 | -|---------|---------|---------|---------| -| 会员信息 | 本地缓存 | 30分钟 | 写时更新 | -| 人脸特征 | 本地缓存 | 24小时 | 定时刷新 | -| 签到规则 | 本地缓存 | 1小时 | 写时更新 | -| 签到统计 | 本地缓存 | 5分钟 | 定时计算 | -| 设备状态 | 本地缓存 | 1分钟 | 心跳更新 | +| -------- | -------- | -------- | -------- | +| 会员信息 | 本地缓存 | 30分钟 | 写时更新 | +| 人脸特征 | 本地缓存 | 24小时 | 定时刷新 | +| 签到规则 | 本地缓存 | 1小时 | 写时更新 | +| 签到统计 | 本地缓存 | 5分钟 | 定时计算 | +| 设备状态 | 本地缓存 | 1分钟 | 心跳更新 | ### 8.2 缓存配置 -```java +````java @Configuration public class CheckinCacheConfig { - + @Bean public Cache memberCache() { return Caffeine.newBuilder() @@ -1723,7 +1723,7 @@ public class CheckinCacheConfig { .recordStats() .build(); } - + @Bean public Cache faceFeatureCache() { return Caffeine.newBuilder() @@ -1732,7 +1732,7 @@ public class CheckinCacheConfig { .recordStats() .build(); } - + @Bean public Cache ruleCache() { return Caffeine.newBuilder() @@ -1753,9 +1753,9 @@ public class CheckinCacheConfig { @Component @RequiredArgsConstructor public class CheckinStatisticsScheduler { - + private final CheckinStatisticsService statisticsService; - + @Scheduled(cron = "0 5 0 * * ?") public void generateYesterdayStatistics() { LocalDate yesterday = LocalDate.now().minusDays(1); @@ -1765,7 +1765,7 @@ public class CheckinStatisticsScheduler { e -> log.error("昨日签到统计生成失败", e) ); } - + @Scheduled(cron = "0 0 */1 * * ?") public void generateTodayStatistics() { LocalDate today = LocalDate.now(); @@ -1776,7 +1776,7 @@ public class CheckinStatisticsScheduler { ); } } -``` +```` ### 9.2 设备心跳检测 @@ -1785,13 +1785,13 @@ public class CheckinStatisticsScheduler { @Component @RequiredArgsConstructor public class DeviceHeartbeatScheduler { - + private final CheckinDeviceRepository deviceRepository; - + @Scheduled(fixedRate = 60000) public void checkDeviceStatus() { LocalDateTime threshold = LocalDateTime.now().minusMinutes(5); - + deviceRepository.findAllByStatus(DeviceStatus.ONLINE) .filter(device -> device.getLastHeartbeat().isBefore(threshold)) .flatMap(device -> { @@ -1814,7 +1814,7 @@ public class DeviceHeartbeatScheduler { ```java public class CheckinException extends RuntimeException { - + public static final String MEMBER_NOT_FOUND = "MEMBER_NOT_FOUND"; public static final String MEMBER_INACTIVE = "MEMBER_INACTIVE"; public static final String BOOKING_NOT_FOUND = "BOOKING_NOT_FOUND"; @@ -1827,19 +1827,19 @@ public class CheckinException extends RuntimeException { public static final String RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"; public static final String CHECKIN_NOT_FOUND = "CHECKIN_NOT_FOUND"; public static final String ALREADY_CANCELLED = "ALREADY_CANCELLED"; - + private final String code; - + public CheckinException(String code) { super(getMessage(code)); this.code = code; } - + public CheckinException(String code, String message) { super(message); this.code = code; } - + private static String getMessage(String code) { return switch (code) { case MEMBER_NOT_FOUND -> "会员不存在"; @@ -1860,24 +1860,24 @@ public class CheckinException extends RuntimeException { } public class FaceException extends RuntimeException { - + public static final String QUALITY_TOO_LOW = "QUALITY_TOO_LOW"; public static final String FACE_ALREADY_REGISTERED = "FACE_ALREADY_REGISTERED"; public static final String FACE_NOT_FOUND = "FACE_NOT_FOUND"; public static final String FACE_NOT_MATCHED = "FACE_NOT_MATCHED"; - + private final String code; - + public FaceException(String code) { super(getMessage(code)); this.code = code; } - + public FaceException(String code, String message) { super(message); this.code = code; } - + private static String getMessage(String code) { return switch (code) { case QUALITY_TOO_LOW -> "人脸质量分数过低"; @@ -1896,14 +1896,14 @@ public class FaceException extends RuntimeException { @Slf4j @RestControllerAdvice public class CheckinExceptionHandler { - + @ExceptionHandler(CheckinException.class) public ResponseEntity> handleCheckinException(CheckinException e) { log.warn("签到异常: {}", e.getMessage()); return ResponseEntity.badRequest() .body(ApiResponse.error(e.getCode(), e.getMessage())); } - + @ExceptionHandler(FaceException.class) public ResponseEntity> handleFaceException(FaceException e) { log.warn("人脸识别异常: {}", e.getMessage()); @@ -1919,51 +1919,51 @@ public class CheckinExceptionHandler { ### 11.1 枚举定义 -| 枚举类型 | 值 | 说明 | -|---------|---|------| -| CheckinType | 1 | 入场签到 | -| CheckinType | 2 | 课程签到 | -| CheckinType | 3 | 私教签到 | -| CheckinType | 4 | 活动签到 | -| CheckinMethod | 1 | 二维码 | -| CheckinMethod | 2 | 人脸识别 | -| CheckinMethod | 3 | NFC | -| CheckinMethod | 4 | 教练代签 | -| CheckinStatus | 1 | 成功 | -| CheckinStatus | 2 | 失败 | -| CheckinStatus | 3 | 已取消 | -| DeviceType | 1 | 人脸识别机 | -| DeviceType | 2 | NFC读卡器 | -| DeviceType | 3 | 扫码枪 | -| DeviceType | 4 | 一体机 | -| DeviceStatus | 1 | 在线 | -| DeviceStatus | 2 | 离线 | -| DeviceStatus | 3 | 维护中 | -| FaceStatus | 1 | 正常 | -| FaceStatus | 2 | 待更新 | -| FaceStatus | 3 | 已禁用 | +| 枚举类型 | 值 | 说明 | +| ------------- | --- | ---------- | +| CheckinType | 1 | 入场签到 | +| CheckinType | 2 | 课程签到 | +| CheckinType | 3 | 私教签到 | +| CheckinType | 4 | 活动签到 | +| CheckinMethod | 1 | 二维码 | +| CheckinMethod | 2 | 人脸识别 | +| CheckinMethod | 3 | NFC | +| CheckinMethod | 4 | 教练代签 | +| CheckinStatus | 1 | 成功 | +| CheckinStatus | 2 | 失败 | +| CheckinStatus | 3 | 已取消 | +| DeviceType | 1 | 人脸识别机 | +| DeviceType | 2 | NFC读卡器 | +| DeviceType | 3 | 扫码枪 | +| DeviceType | 4 | 一体机 | +| DeviceStatus | 1 | 在线 | +| DeviceStatus | 2 | 离线 | +| DeviceStatus | 3 | 维护中 | +| FaceStatus | 1 | 正常 | +| FaceStatus | 2 | 待更新 | +| FaceStatus | 3 | 已禁用 | ### 11.2 错误码定义 -| 错误码 | 说明 | 处理建议 | -|-------|------|---------| -| MEMBER_NOT_FOUND | 会员不存在 | 检查会员ID | -| MEMBER_INACTIVE | 会员状态异常 | 联系工作人员 | -| BOOKING_NOT_FOUND | 预约不存在 | 检查预约ID | -| BOOKING_NOT_MATCH | 预约不匹配 | 确认预约信息 | -| BOOKING_NOT_CONFIRMED | 预约未确认 | 等待确认 | -| ALREADY_CHECKED | 已签到 | 无需重复签到 | -| COURSE_ENDED | 课程已结束 | 无法签到 | -| DAILY_LIMIT_EXCEEDED | 签到次数超限 | 明日再来 | -| INTERVAL_NOT_MET | 签到间隔不足 | 稍后重试 | -| RATE_LIMIT_EXCEEDED | 请求过于频繁 | 稍后重试 | -| QUALITY_TOO_LOW | 人脸质量低 | 重新拍照 | -| FACE_NOT_MATCHED | 人脸匹配失败 | 重新注册或使用其他方式 | +| 错误码 | 说明 | 处理建议 | +| --------------------- | ------------ | ---------------------- | +| MEMBER_NOT_FOUND | 会员不存在 | 检查会员ID | +| MEMBER_INACTIVE | 会员状态异常 | 联系工作人员 | +| BOOKING_NOT_FOUND | 预约不存在 | 检查预约ID | +| BOOKING_NOT_MATCH | 预约不匹配 | 确认预约信息 | +| BOOKING_NOT_CONFIRMED | 预约未确认 | 等待确认 | +| ALREADY_CHECKED | 已签到 | 无需重复签到 | +| COURSE_ENDED | 课程已结束 | 无法签到 | +| DAILY_LIMIT_EXCEEDED | 签到次数超限 | 明日再来 | +| INTERVAL_NOT_MET | 签到间隔不足 | 稍后重试 | +| RATE_LIMIT_EXCEEDED | 请求过于频繁 | 稍后重试 | +| QUALITY_TOO_LOW | 人脸质量低 | 重新拍照 | +| FACE_NOT_MATCHED | 人脸匹配失败 | 重新注册或使用其他方式 | --- ## 十二、版本历史 -| 版本 | 日期 | 作者 | 变更内容 | -|------|------|------|---------| -| v1.0 | 2026-02-28 | 张翔 | 初稿 | +| 版本 | 日期 | 作者 | 变更内容 | +| ---- | ---------- | ---- | -------- | +| v1.0 | 2026-02-28 | 张翔 | 初稿 | diff --git a/docs/design/LLD-预约模块详细设计.md b/docs/design/LLD-预约模块详细设计.md index 4bd06d1..d76db30 100644 --- a/docs/design/LLD-预约模块详细设计.md +++ b/docs/design/LLD-预约模块详细设计.md @@ -44,28 +44,28 @@ │ 预约模块边界 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 预约模块内部 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 课程管理 • 时段管理 • 预约管理 • 库存管理 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 预约模块内部 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 课程管理 • 时段管理 • 预约管理 • 库存管理 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 外部依赖 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 会员模块 (查询会员权益、扣减权益) │ │ -│ │ • 教练模块 (查询教练信息、排班) │ │ -│ │ • 场地模块 (查询场地信息、可用性) │ │ -│ │ • 消息模块 (发送预约通知) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 外部依赖 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 会员模块 (查询会员权益、扣减权益) │ │ +│ │ • 教练模块 (查询教练信息、排班) │ │ +│ │ • 场地模块 (查询场地信息、可用性) │ │ +│ │ • 消息模块 (发送预约通知) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 被依赖 │ │ -│ ├─────────────────────────────────────────────────────────────────┤ │ -│ │ • 签到模块 (查询预约信息、验证签到资格) │ │ -│ │ • 财务模块 (查询预约消费记录) │ │ -│ │ • 数据模块 (预约数据分析) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ 被依赖 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 签到模块 (查询预约信息、验证签到资格) │ │ +│ │ • 财务模块 (查询预约消费记录) │ │ +│ │ • 数据模块 (预约数据分析) │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -81,25 +81,25 @@ │ 实体关系图 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ coach │ │ course │ │ venue │ │ -│ │ (教练) │ │ (课程) │ │ (场地) │ │ -│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ -│ │ 1:N │ 1:N │ 1:N │ -│ │ │ │ │ -│ └──────────────┴──────────────┘ │ -│ │ 1:N │ -│ ▼ │ -│ ┌──────────────────┐ │ -│ │ booking_slot │ │ -│ │ (预约时段) │ │ -│ └────────┬─────────┘ │ -│ │ 1:N │ -│ ▼ │ -│ ┌──────────────────┐ │ -│ │ booking_record │ │ -│ │ (预约记录) │ │ -│ └──────────────────┘ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ coach │ │ course │ │ venue │ │ +│ │ (教练) │ │ (课程) │ │ (场地) │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +│ │ 1:N │ 1:N │ 1:N │ +│ │ │ │ │ +│ └──────────────┴──────────────┘ │ +│ │ 1:N │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ booking_slot │ │ +│ │ (预约时段) │ │ +│ └────────┬─────────┘ │ +│ │ 1:N │ +│ ▼ │ +│ ┌──────────────────┐ │ +│ │ booking_record │ │ +│ │ (预约记录) │ │ +│ └──────────────────┘ │ │ │ │ 关系说明: │ │ • coach (1) ─── (N) booking_slot : 一个教练有多个时段 │ @@ -334,75 +334,75 @@ CREATE INDEX idx_schedule_date ON coach_schedule(schedule_date) WHERE deleted_at │ 预约领域模型 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ <> │ │ -│ │ BookingSlot │ │ -│ ├───────────────────────────────────────────────────────────────────┤ │ -│ │ - id: Long │ │ -│ │ - tenantId: Long │ │ -│ │ - storeId: Long │ │ -│ │ - resourceType: ResourceType │ │ -│ │ - resourceId: Long │ │ -│ │ - coachId: Long │ │ -│ │ - venueId: Long │ │ -│ │ - startTime: LocalDateTime │ │ -│ │ - endTime: LocalDateTime │ │ -│ │ - capacity: Integer │ │ -│ │ - bookedCount: Integer │ │ -│ │ - waitlistCount: Integer │ │ -│ │ - status: SlotStatus │ │ -│ │ - version: Integer │ │ -│ ├───────────────────────────────────────────────────────────────────┤ │ -│ │ + hasCapacity(): Boolean │ │ -│ │ + getRemainCapacity(): Integer │ │ -│ │ + canBook(): Boolean │ │ -│ │ + book(): void │ │ -│ │ + cancel(): void │ │ -│ │ + isExpired(): Boolean │ │ -│ │ + isFull(): Boolean │ │ -│ │ + addToWaitlist(): void │ │ -│ │ + removeFromWaitlist(): void │ │ -│ └───────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ │ 1:N │ -│ ▼ │ -│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ -│ │ <> │ │ <> │ │ -│ │ BookingRecord │ │ BookingWaitlist │ │ -│ ├────────────────────────────┤ ├────────────────────────────┤ │ -│ │ - id: Long │ │ - id: Long │ │ -│ │ - memberId: Long │ │ - memberId: Long │ │ -│ │ - slotId: Long │ │ - slotId: Long │ │ -│ │ - bookingNo: String │ │ - queueNo: Integer │ │ -│ │ - status: BookingStatus │ │ - status: WaitlistStatus │ │ -│ │ - priceType: PriceType │ │ - expireAt: LocalDateTime │ │ -│ │ - priceValue: BigDecimal │ ├────────────────────────────┤ │ -│ │ - checkinStatus: CheckinSt │ │ + isExpired(): Boolean │ │ -│ ├────────────────────────────┤ │ + convert(): void │ │ -│ │ + canCancel(): Boolean │ └────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ <> │ │ +│ │ BookingSlot │ │ +│ ├───────────────────────────────────────────────────────────────────┤ │ +│ │ - id: Long │ │ +│ │ - tenantId: Long │ │ +│ │ - storeId: Long │ │ +│ │ - resourceType: ResourceType │ │ +│ │ - resourceId: Long │ │ +│ │ - coachId: Long │ │ +│ │ - venueId: Long │ │ +│ │ - startTime: LocalDateTime │ │ +│ │ - endTime: LocalDateTime │ │ +│ │ - capacity: Integer │ │ +│ │ - bookedCount: Integer │ │ +│ │ - waitlistCount: Integer │ │ +│ │ - status: SlotStatus │ │ +│ │ - version: Integer │ │ +│ ├───────────────────────────────────────────────────────────────────┤ │ +│ │ + hasCapacity(): Boolean │ │ +│ │ + getRemainCapacity(): Integer │ │ +│ │ + canBook(): Boolean │ │ +│ │ + book(): void │ │ +│ │ + cancel(): void │ │ +│ │ + isExpired(): Boolean │ │ +│ │ + isFull(): Boolean │ │ +│ │ + addToWaitlist(): void │ │ +│ │ + removeFromWaitlist(): void │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ │ 1:N │ +│ ▼ │ +│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ +│ │ <> │ │ <> │ │ +│ │ BookingRecord │ │ BookingWaitlist │ │ +│ ├────────────────────────────┤ ├────────────────────────────┤ │ +│ │ - id: Long │ │ - id: Long │ │ +│ │ - memberId: Long │ │ - memberId: Long │ │ +│ │ - slotId: Long │ │ - slotId: Long │ │ +│ │ - bookingNo: String │ │ - queueNo: Integer │ │ +│ │ - status: BookingStatus │ │ - status: WaitlistStatus │ │ +│ │ - priceType: PriceType │ │ - expireAt: LocalDateTime │ │ +│ │ - priceValue: BigDecimal │ ├────────────────────────────┤ │ +│ │ - checkinStatus: CheckinSt │ │ + isExpired(): Boolean │ │ +│ ├────────────────────────────┤ │ + convert(): void │ │ +│ │ + canCancel(): Boolean │ └────────────────────────────┘ │ │ │ + cancel(): void │ │ │ │ + checkin(): void │ │ │ │ + isCheckinable(): Boolean │ │ │ └────────────────────────────┘ │ │ │ -│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ -│ │ <> │ │ <> │ │ -│ │ ResourceType │ │ SlotStatus │ │ -│ ├────────────────────────────┤ ├────────────────────────────┤ │ -│ │ GROUP_CLASS(1, "团课") │ │ AVAILABLE(1, "可预约") │ │ -│ │ PRIVATE(2, "私教") │ │ FULL(2, "已满") │ │ -│ │ VENUE(3, "场地") │ │ CANCELLED(3, "已取消") │ │ -│ │ ONLINE(4, "线上") │ │ ENDED(4, "已结束") │ │ -│ └────────────────────────────┘ └────────────────────────────┘ │ +│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ +│ │ <> │ │ <> │ │ +│ │ ResourceType │ │ SlotStatus │ │ +│ ├────────────────────────────┤ ├────────────────────────────┤ │ +│ │ GROUP_CLASS(1, "团课") │ │ AVAILABLE(1, "可预约") │ │ +│ │ PRIVATE(2, "私教") │ │ FULL(2, "已满") │ │ +│ │ VENUE(3, "场地") │ │ CANCELLED(3, "已取消") │ │ +│ │ ONLINE(4, "线上") │ │ ENDED(4, "已结束") │ │ +│ └────────────────────────────┘ └────────────────────────────┘ │ │ │ -│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ -│ │ <> │ │ <> │ │ -│ │ BookingStatus │ │ PriceType │ │ -│ ├────────────────────────────┤ ├────────────────────────────┤ │ -│ │ BOOKED(1, "已预约") │ │ TIMES(1, "扣次") │ │ -│ │ CANCELLED(2, "已取消") │ │ DURATION(2, "扣时长") │ │ -│ │ COMPLETED(3, "已完成") │ │ AMOUNT(3, "扣金额") │ │ -│ │ EXPIRED(4, "已过期") │ └────────────────────────────┘ │ +│ ┌────────────────────────────┐ ┌────────────────────────────┐ │ +│ │ <> │ │ <> │ │ +│ │ BookingStatus │ │ PriceType │ │ +│ ├────────────────────────────┤ ├────────────────────────────┤ │ +│ │ BOOKED(1, "已预约") │ │ TIMES(1, "扣次") │ │ +│ │ CANCELLED(2, "已取消") │ │ DURATION(2, "扣时长") │ │ +│ │ COMPLETED(3, "已完成") │ │ AMOUNT(3, "扣金额") │ │ +│ │ EXPIRED(4, "已过期") │ └────────────────────────────┘ │ │ └────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ @@ -415,40 +415,40 @@ CREATE INDEX idx_schedule_date ON coach_schedule(schedule_date) WHERE deleted_at │ 领域服务设计 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ <> │ │ -│ │ BookingDomainService │ │ -│ ├───────────────────────────────────────────────────────────────────┤ │ -│ │ + createBooking(command: CreateBookingCommand): BookingRecord │ │ -│ │ + cancelBooking(bookingId: Long, reason: String): void │ │ -│ │ + checkin(bookingId: Long): void │ │ -│ │ + addToWaitlist(memberId: Long, slotId: Long): void │ │ -│ │ + processWaitlist(slotId: Long): void │ │ -│ │ + validateBooking(memberId: Long, slotId: Long): void │ │ -│ └───────────────────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ <> │ │ +│ │ BookingDomainService │ │ +│ ├───────────────────────────────────────────────────────────────────┤ │ +│ │ + createBooking(command: CreateBookingCommand): BookingRecord │ │ +│ │ + cancelBooking(bookingId: Long, reason: String): void │ │ +│ │ + checkin(bookingId: Long): void │ │ +│ │ + addToWaitlist(memberId: Long, slotId: Long): void │ │ +│ │ + processWaitlist(slotId: Long): void │ │ +│ │ + validateBooking(memberId: Long, slotId: Long): void │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ <> │ │ -│ │ SlotDomainService │ │ -│ ├───────────────────────────────────────────────────────────────────┤ │ -│ │ + createSlot(command: CreateSlotCommand): BookingSlot │ │ -│ │ + batchCreateSlots(command: BatchSlotCommand): List │ │ -│ │ + updateSlot(slotId: Long, command: UpdateSlotCommand): void │ │ -│ │ + cancelSlot(slotId: Long, reason: String): void │ │ -│ │ + getAvailableSlots(query: SlotQuery): List │ │ -│ │ + incrementBookedCount(slotId: Long): Boolean │ │ -│ │ + decrementBookedCount(slotId: Long): void │ │ -│ └───────────────────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ <> │ │ +│ │ SlotDomainService │ │ +│ ├───────────────────────────────────────────────────────────────────┤ │ +│ │ + createSlot(command: CreateSlotCommand): BookingSlot │ │ +│ │ + batchCreateSlots(command: BatchSlotCommand): List │ │ +│ │ + updateSlot(slotId: Long, command: UpdateSlotCommand): void │ │ +│ │ + cancelSlot(slotId: Long, reason: String): void │ │ +│ │ + getAvailableSlots(query: SlotQuery): List │ │ +│ │ + incrementBookedCount(slotId: Long): Boolean │ │ +│ │ + decrementBookedCount(slotId: Long): void │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ <> │ │ -│ │ InventoryDomainService │ │ -│ ├───────────────────────────────────────────────────────────────────┤ │ -│ │ + checkInventory(slotId: Long): Boolean │ │ -│ │ + reserveInventory(slotId: Long, count: Integer): Boolean │ │ -│ │ + releaseInventory(slotId: Long, count: Integer): void │ │ -│ │ + preloadInventory(slotIds: List): void │ │ -│ └───────────────────────────────────────────────────────────────────┘ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ <> │ │ +│ │ InventoryDomainService │ │ +│ ├───────────────────────────────────────────────────────────────────┤ │ +│ │ + checkInventory(slotId: Long): Boolean │ │ +│ │ + reserveInventory(slotId: Long, count: Integer): Boolean │ │ +│ │ + releaseInventory(slotId: Long, count: Integer): void │ │ +│ │ + preloadInventory(slotIds: List): void │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -464,50 +464,50 @@ CREATE INDEX idx_schedule_date ON coach_schedule(schedule_date) WHERE deleted_at │ 团课预约流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ 会员端 API层 BookingService BenefitService 数据层 │ -│ │ │ │ │ │ │ -│ │ 1.选择课程 │ │ │ │ │ -│ │─────────▶│ │ │ │ │ -│ │ │ 2.查询时段 │ │ │ │ -│ │ │─────────────▶│ │ │ │ -│ │ │ │ 3.查询可预约时段 │ │ │ -│ │ │ │───────────────────────────────▶│ │ -│ │ │ │◀───────────────────────────────│ │ -│ │◀─────────│ 返回时段列表 │ │ │ │ -│ │ │ │ │ │ │ -│ │ 4.提交预约 │ │ │ │ │ -│ │─────────▶│ │ │ │ │ -│ │ │ 5.创建预约 │ │ │ │ -│ │ │─────────────▶│ │ │ │ -│ │ │ │ 6.校验时段 │ │ │ -│ │ │ │───────────────────────────────▶│ │ -│ │ │ │◀───────────────────────────────│ │ -│ │ │ │ │ │ │ -│ │ │ │ 7.校验会员状态 │ │ │ -│ │ │ │───────────────────────────────▶│ │ -│ │ │ │◀───────────────────────────────│ │ -│ │ │ │ │ │ │ -│ │ │ │ 8.检查库存(原子)│ │ │ -│ │ │ │───────────────────────────────▶│ │ -│ │ │ │◀───────────────────────────────│ │ -│ │ │ │ │ │ │ -│ │ │ │ 9.扣减权益 │ │ │ -│ │ │ │────────────────▶│ │ │ -│ │ │ │ │─────────────▶│ │ -│ │ │ │ │◀─────────────│ │ -│ │ │ │◀────────────────│ │ │ -│ │ │ │ │ │ │ -│ │ │ │ 10.创建预约记录 │ │ │ -│ │ │ │───────────────────────────────▶│ │ -│ │ │ │ │ │ │ -│ │ │ │ 11.增加预约人数 │ │ │ -│ │ │ │───────────────────────────────▶│ │ -│ │ │ │◀───────────────────────────────│ │ -│ │ │ │ │ │ │ -│ │ │ │ 12.发送预约通知 │ │ │ -│ │◀─────────│◀─────────────│ │ │ │ -│ │ 返回预约成功│ │ │ │ │ -│ │ │ │ │ │ │ +│ 会员端 API层 BookingService BenefitService 数据层 │ +│ │ │ │ │ │ │ +│ │ 1.选择课程 │ │ │ │ │ +│ │─────────▶│ │ │ │ │ +│ │ │ 2.查询时段 │ │ │ │ +│ │ │─────────────▶│ │ │ │ +│ │ │ │ 3.查询可预约时段 │ │ │ +│ │ │ │───────────────────────────────▶│ │ +│ │ │ │◀───────────────────────────────│ │ +│ │◀─────────│ 返回时段列表 │ │ │ │ +│ │ │ │ │ │ │ +│ │ 4.提交预约 │ │ │ │ │ +│ │─────────▶│ │ │ │ │ +│ │ │ 5.创建预约 │ │ │ │ +│ │ │─────────────▶│ │ │ │ +│ │ │ │ 6.校验时段 │ │ │ +│ │ │ │───────────────────────────────▶│ │ +│ │ │ │◀───────────────────────────────│ │ +│ │ │ │ │ │ │ +│ │ │ │ 7.校验会员状态 │ │ │ +│ │ │ │───────────────────────────────▶│ │ +│ │ │ │◀───────────────────────────────│ │ +│ │ │ │ │ │ │ +│ │ │ │ 8.检查库存(原子)│ │ │ +│ │ │ │───────────────────────────────▶│ │ +│ │ │ │◀───────────────────────────────│ │ +│ │ │ │ │ │ │ +│ │ │ │ 9.扣减权益 │ │ │ +│ │ │ │────────────────▶│ │ │ +│ │ │ │ │─────────────▶│ │ +│ │ │ │ │◀─────────────│ │ +│ │ │ │◀────────────────│ │ │ +│ │ │ │ │ │ │ +│ │ │ │ 10.创建预约记录 │ │ │ +│ │ │ │───────────────────────────────▶│ │ +│ │ │ │ │ │ │ +│ │ │ │ 11.增加预约人数 │ │ │ +│ │ │ │───────────────────────────────▶│ │ +│ │ │ │◀───────────────────────────────│ │ +│ │ │ │ │ │ │ +│ │ │ │ 12.发送预约通知 │ │ │ +│ │◀─────────│◀─────────────│ │ │ │ +│ │ 返回预约成功│ │ │ │ │ +│ │ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -519,41 +519,41 @@ CREATE INDEX idx_schedule_date ON coach_schedule(schedule_date) WHERE deleted_at │ 取消预约流程 │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ -│ 会员端 API层 BookingService BenefitService 数据层 │ -│ │ │ │ │ │ │ -│ │ 1.请求取消 │ │ │ │ │ -│ │─────────▶│ │ │ │ │ -│ │ │ 2.查询预约 │ │ │ │ -│ │ │─────────────▶│ │ │ │ -│ │ │ │───────────────────────────────▶│ │ -│ │ │ │◀───────────────────────────────│ │ -│ │ │ │ │ │ │ -│ │ │ │ 3.校验可取消 │ │ │ -│ │ │ │ - 状态检查 │ │ │ -│ │ │ │ - 时间检查 │ │ │ -│ │ │ │ │ │ │ -│ │ │ │ 4.计算退款金额 │ │ │ -│ │ │ │ │ │ │ -│ │ │ │ 5.退还权益 │ │ │ -│ │ │ │────────────────▶│ │ │ -│ │ │ │ │─────────────▶│ │ -│ │ │ │ │◀─────────────│ │ -│ │ │ │◀────────────────│ │ │ -│ │ │ │ │ │ │ -│ │ │ │ 6.更新预约状态 │ │ │ -│ │ │ │───────────────────────────────▶│ │ -│ │ │ │ │ │ │ -│ │ │ │ 7.减少预约人数 │ │ │ -│ │ │ │───────────────────────────────▶│ │ -│ │ │ │ │ │ │ -│ │ │ │ 8.处理候补队列 │ │ │ -│ │ │ │───────────────────────────────▶│ │ -│ │ │ │◀───────────────────────────────│ │ -│ │ │ │ │ │ │ -│ │ │ │ 9.发送取消通知 │ │ │ -│ │◀─────────│◀─────────────│ │ │ │ -│ │ 返回取消成功│ │ │ │ │ -│ │ │ │ │ │ │ +│ 会员端 API层 BookingService BenefitService 数据层 │ +│ │ │ │ │ │ │ +│ │ 1.请求取消 │ │ │ │ │ +│ │─────────▶│ │ │ │ │ +│ │ │ 2.查询预约 │ │ │ │ +│ │ │─────────────▶│ │ │ │ +│ │ │ │───────────────────────────────▶│ │ +│ │ │ │◀───────────────────────────────│ │ +│ │ │ │ │ │ │ +│ │ │ │ 3.校验可取消 │ │ │ +│ │ │ │ - 状态检查 │ │ │ +│ │ │ │ - 时间检查 │ │ │ +│ │ │ │ │ │ │ +│ │ │ │ 4.计算退款金额 │ │ │ +│ │ │ │ │ │ │ +│ │ │ │ 5.退还权益 │ │ │ +│ │ │ │────────────────▶│ │ │ +│ │ │ │ │─────────────▶│ │ +│ │ │ │ │◀─────────────│ │ +│ │ │ │◀────────────────│ │ │ +│ │ │ │ │ │ │ +│ │ │ │ 6.更新预约状态 │ │ │ +│ │ │ │───────────────────────────────▶│ │ +│ │ │ │ │ │ │ +│ │ │ │ 7.减少预约人数 │ │ │ +│ │ │ │───────────────────────────────▶│ │ +│ │ │ │ │ │ │ +│ │ │ │ 8.处理候补队列 │ │ │ +│ │ │ │───────────────────────────────▶│ │ +│ │ │ │◀───────────────────────────────│ │ +│ │ │ │ │ │ │ +│ │ │ │ 9.发送取消通知 │ │ │ +│ │◀─────────│◀─────────────│ │ │ │ +│ │ 返回取消成功│ │ │ │ │ +│ │ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ```