From 971d51cb362834487a26db50b4264fdeb068c174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=BF=94?= Date: Sat, 7 Mar 2026 16:59:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8C=E6=AD=A5UI=E6=A8=A1=E7=89=88?= =?UTF-8?q?=E5=AE=9A=E5=88=B6=E5=8A=9F=E8=83=BD=E5=88=B0PRD=E3=80=81HLD?= =?UTF-8?q?=E3=80=81LLD=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRD更新: - 新增2.6 UI模版定制模块 - 包含品牌定制、布局调整、预设模板、配置历史、可视化配置器五个子模块 - 每个子模块包含功能描述、用户故事、功能点、业务规则、验收标准 HLD更新: - 业务范围中新增UI模版定制模块 - 新增3.5 UI模版定制流程(业务场景、业务流程、业务规则、异常处理) - 新增4.6 UI模版定制规则(品牌元素应用、Logo格式限制、颜色格式限制等8条规则) LLD更新: - 新增2.6 UI模版定制模块(模块概述、数据模型设计、核心业务逻辑) - 数据模型包含4个表:tenant_ui_config、ui_template、ui_config_history、ui_resource - 核心业务逻辑包含4个Service:BrandConfigService、LayoutConfigService、TemplateService、ConfigHistoryService - 新增3.5 UI模版定制模块API(10个API接口,涵盖品牌定制、布局调整、模板管理、配置历史) 所有文档已保持一致性,UI模版定制功能已完整同步到产品需求、概要设计、详细设计文档中 --- docs/design/HLD-基础版系统概要设计.md | 234 +++++--- docs/design/LLD-基础版系统详细设计.md | 739 ++++++++++++++++++++++++- docs/product/PRD-基础版产品设计文档.md | 136 ++++- 3 files changed, 1011 insertions(+), 98 deletions(-) diff --git a/docs/design/HLD-基础版系统概要设计.md b/docs/design/HLD-基础版系统概要设计.md index a6cb7c9..d730538 100644 --- a/docs/design/HLD-基础版系统概要设计.md +++ b/docs/design/HLD-基础版系统概要设计.md @@ -10,8 +10,8 @@ ## 文档修订历史 -| 版本 | 日期 | 作者 | 修订内容 | -| ---- | ---------- | ---- | ------------------ | +| 版本 | 日期 | 作者 | 修订内容 | +| ---- | ---------- | ---- | ---------------------- | | v1.0 | 2026-03-04 | 张翔 | 创建基础版业务概要设计 | --- @@ -32,19 +32,18 @@ ### 1.3 术语定义 -| 术语 | 定义 | +| 术语 | 定义 | | ----------------------------- | ------------------------------------------------ | -| 租户(Tenant) | 系统的多租户架构中的独立业务实体,如一个连锁品牌 | -| 门店(Store) | 租户下的具体经营场所 | -| 会员(Member) | 在门店注册的用户 | -| 权益(Benefit) | 会员卡包含的时长、次数、储值、等级等权益 | -| 可预约资源(Bookable Resource) | 团课等可被预约的对象 | -| 时段(Slot) | 资源的可预约时间窗口 | +| 租户(Tenant) | 系统的多租户架构中的独立业务实体,如一个连锁品牌 | +| 门店(Store) | 租户下的具体经营场所 | +| 会员(Member) | 在门店注册的用户 | +| 权益(Benefit) | 会员卡包含的时长、次数、储值、等级等权益 | +| 可预约资源(Bookable Resource) | 团课等可被预约的对象 | +| 时段(Slot) | 资源的可预约时间窗口 | ### 1.4 参考文档 - 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001 -- 《健身房管理系统业务概要设计文档》 GYM-HLD-001 --- @@ -52,21 +51,21 @@ ### 2.1 业务目标 -| 目标维度 | 目标描述 | 成功指标 | +| 目标维度 | 目标描述 | 成功指标 | | -------- | ---------------------- | -------------------------------- | | 用户体验 | 提升会员预约和签到体验 | 预约成功率 ≥ 95%,签到耗时 ≤ 3秒 | -| 运营效率 | 降低人工操作成本 | 人工处理时间减少 50% | -| 数据价值 | 提供基础数据支持 | 数据报表使用率 ≥ 80% | +| 运营效率 | 降低人工操作成本 | 人工处理时间减少 50% | +| 数据价值 | 提供基础数据支持 | 数据报表使用率 ≥ 80% | ### 2.2 用户角色 -| 角色 | 描述 | 主要功能 | +| 角色 | 描述 | 主要功能 | | ---------- | -------------- | ---------------------------- | -| 会员 | 健身房注册用户 | 预约课程、签到、查看个人信息 | -| 教练 | 健身房教练 | 排课、团课签到管理 | -| 前台 | 门店前台人员 | 会员接待、签到辅助、会员管理 | -| 店长 | 门店管理者 | 单店全功能管理、数据查看 | -| 超级管理员 | 平台最高权限 | 全平台管理、系统配置 | +| 会员 | 健身房注册用户 | 预约课程、签到、查看个人信息 | +| 教练 | 健身房教练 | 排课、团课签到管理 | +| 前台 | 门店前台人员 | 会员接待、签到辅助、会员管理 | +| 店长 | 门店管理者 | 单店全功能管理、数据查看 | +| 超级管理员 | 平台最高权限 | 全平台管理、系统配置 | ### 2.3 业务范围 @@ -105,6 +104,12 @@ │ │ • 用户管理 • 角色权限管理 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ +│ ┌─────────────────────────────────────────────────────────────────┐ │ +│ │ UI模版定制 │ │ +│ ├─────────────────────────────────────────────────────────────────┤ │ +│ │ • 品牌定制 • 布局调整 • 预设模板 • 配置历史 │ │ +│ └─────────────────────────────────────────────────────────────────┘ │ +│ │ └─────────────────────────────────────────────────────────────────────────┘ ``` @@ -136,11 +141,11 @@ #### 3.1.4 异常处理 -| 异常场景 | 处理方式 | -|---------|---------| +| 异常场景 | 处理方式 | +| ------------ | ---------------- | | 手机号已存在 | 提示用户直接登录 | -| 验证码错误 | 提示用户重新输入 | -| 验证码过期 | 提示用户重新获取 | +| 验证码错误 | 提示用户重新输入 | +| 验证码过期 | 提示用户重新获取 | --- @@ -183,11 +188,11 @@ #### 3.2.4 异常处理 -| 异常场景 | 处理方式 | -|---------|---------| -| 课程已满 | 提示用户选择其他课程 | -| 会员卡权益不足 | 提示用户购买会员卡 | -| 预约时间过短 | 提示用户提前预约 | +| 异常场景 | 处理方式 | +| -------------- | -------------------- | +| 课程已满 | 提示用户选择其他课程 | +| 会员卡权益不足 | 提示用户购买会员卡 | +| 预约时间过短 | 提示用户提前预约 | --- @@ -215,11 +220,11 @@ #### 3.3.4 异常处理 -| 异常场景 | 处理方式 | -|---------|---------| +| 异常场景 | 处理方式 | +| ---------- | ------------------ | | 会员卡无效 | 提示用户购买会员卡 | -| 会员卡过期 | 提示用户续费 | -| 签到码无效 | 提示用户重新扫描 | +| 会员卡过期 | 提示用户续费 | +| 签到码无效 | 提示用户重新扫描 | --- @@ -247,56 +252,103 @@ #### 3.4.4 异常处理 -| 异常场景 | 处理方式 | -|---------|---------| -| 支付失败 | 提示用户重新支付 | +| 异常场景 | 处理方式 | +| -------- | -------------------- | +| 支付失败 | 提示用户重新支付 | | 支付超时 | 提示用户重新发起支付 | --- +### 3.5 UI模版定制流程 + +#### 3.5.1 业务场景 + +租户通过管理后台的可视化配置器定制自己的UI,包括品牌元素、布局结构和预设模板。 + +#### 3.5.2 业务流程 + +``` +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ +│ 租户登录 │ → │ 打开UI │ → │ 品牌定制 │ → │ 布局调整 │ → │ 配置保存 │ +│ 管理后台 │ │ 定制器 │ │ │ │ │ │ │ +└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ +``` + +#### 3.5.3 业务规则 + +- 品牌元素应用范围包括小程序和管理后台 +- 布局调整支持拖拽排序和模块隐藏 +- 预设模板应用后保留品牌配置 +- 配置变更实时生效,无需重新部署 +- 配置变更自动记录到历史 + +#### 3.5.4 异常处理 + +| 异常场景 | 处理方式 | +| ------------ | -------------------- | +| Logo上传失败 | 提示用户重新上传 | +| 配置保存失败 | 提示用户检查配置格式 | +| 模板应用失败 | 提示用户调整品牌配置 | +| 配置回滚失败 | 提示用户选择其他版本 | + +--- + ## 四、核心业务规则 ### 4.1 会员管理规则 -| 规则 | 描述 | -|------|------| -| 会员唯一性 | 手机号作为会员唯一标识 | -| 会员信息完整性 | 必填字段:手机号、姓名、性别 | +| 规则 | 描述 | +| ---------------- | ------------------------------------------------------ | +| 会员唯一性 | 手机号作为会员唯一标识 | +| 会员信息完整性 | 必填字段:手机号、姓名、性别 | | 会员信息修改权限 | 会员只能编辑自己的基本信息,前台和店长可以编辑所有信息 | ### 4.2 会员卡管理规则 -| 规则 | 描述 | -|------|------| -| 会员卡类型 | 支持时长卡、次卡、储值卡 | -| 会员卡有效期 | 时长卡有有效期,次卡和储值卡无有效期 | -| 会员卡到期提醒 | 到期前7天提醒 | -| 会员卡续费 | 续费后权益立即生效 | +| 规则 | 描述 | +| -------------- | ------------------------------------ | +| 会员卡类型 | 支持时长卡、次卡、储值卡 | +| 会员卡有效期 | 时长卡有有效期,次卡和储值卡无有效期 | +| 会员卡到期提醒 | 到期前7天提醒 | +| 会员卡续费 | 续费后权益立即生效 | ### 4.3 预约管理规则 -| 规则 | 描述 | -|------|------| -| 预约时间限制 | 预约需在课程开始前至少30分钟 | +| 规则 | 描述 | +| ---------------- | ------------------------------- | +| 预约时间限制 | 预约需在课程开始前至少30分钟 | | 取消预约时间限制 | 取消预约需在课程开始前至少2小时 | -| 团课容量限制 | 每节课最多20人 | -| 预约权益扣减 | 预约成功后扣减权益 | +| 团课容量限制 | 每节课最多20人 | +| 预约权益扣减 | 预约成功后扣减权益 | ### 4.4 签到管理规则 -| 规则 | 描述 | -|------|------| -| 签到验证 | 签到需验证会员卡有效性 | +| 规则 | 描述 | +| ------------ | -------------------------- | +| 签到验证 | 签到需验证会员卡有效性 | | 签到预约验证 | 签到需验证预约信息(如有) | -| 签到记录 | 签到成功后记录到店时间 | +| 签到记录 | 签到成功后记录到店时间 | ### 4.5 数据统计规则 -| 规则 | 描述 | -|------|------| -| 数据保留期限 | 数据保留30天 | -| 统计维度 | 支持按日、周、月统计 | -| 数据导出 | 支持数据导出 | +| 规则 | 描述 | +| ------------ | -------------------- | +| 数据保留期限 | 数据保留30天 | +| 统计维度 | 支持按日、周、月统计 | +| 数据导出 | 支持数据导出 | + +### 4.6 UI模版定制规则 + +| 规则 | 描述 | +| ------------ | ------------------------------------------ | +| 品牌元素应用 | 品牌元素应用范围包括小程序和管理后台 | +| Logo格式限制 | Logo支持PNG/JPG格式,限制2MB以内 | +| 颜色格式限制 | 颜色支持RGB和HEX格式 | +| 布局调整权限 | 布局调整支持按角色区分(店长、前台、会员) | +| 模板应用规则 | 模板应用后保留租户已有的品牌配置 | +| 配置版本管理 | 每次配置变更自动生成新版本号 | +| 配置历史保留 | 配置历史保留90天 | +| 配置实时生效 | 配置变更实时生效,无需重新部署 | --- @@ -397,14 +449,14 @@ ### 6.1 核心实体 -| 实体 | 描述 | -|------|------| -| 会员(Member) | 健身房注册用户 | +| 实体 | 描述 | +| ------------------ | ---------------- | +| 会员(Member) | 健身房注册用户 | | 会员卡(MemberCard) | 会员购买的权益卡 | -| 权益(Benefit) | 会员卡包含的权益 | -| 团课(GroupClass) | 集体课程 | -| 预约(Booking) | 会员预约记录 | -| 签到(CheckIn) | 会员签到记录 | +| 权益(Benefit) | 会员卡包含的权益 | +| 团课(GroupClass) | 集体课程 | +| 预约(Booking) | 会员预约记录 | +| 签到(CheckIn) | 会员签到记录 | ### 6.2 实体关系 @@ -422,36 +474,35 @@ ### 7.1 性能约束 -| 指标 | 要求 | -|------|------| -| API响应时间 (P99) | 200-400ms | -| 并发用户 | 支持1000并发用户 | -| 吞吐量 (QPS) | 3000-5000 | -| 数据库查询 | 查询响应时间 ≤ 500ms | +| 指标 | 要求 | +| ----------------- | ----------------- | +| API响应时间 (P99) | ≤ 500ms | +| 并发用户 | 支持100并发用户 | +| 数据库查询 | 查询响应时间 ≤ 1s | ### 7.2 可用性约束 -| 指标 | 要求 | -|------|------| -| 系统可用性 | SLA ≥ 99.9% | +| 指标 | 要求 | +| ------------ | ------------- | +| 系统可用性 | SLA ≥ 99.9% | | 故障恢复时间 | MTTR ≤ 30分钟 | ### 7.3 安全性约束 -| 指标 | 要求 | -|------|------| -| 数据加密 | 敏感数据加密存储 | -| 访问控制 | 基于角色的访问控制 | +| 指标 | 要求 | +| -------- | -------------------- | +| 数据加密 | 敏感数据加密存储 | +| 访问控制 | 基于角色的访问控制 | | 操作审计 | 关键操作记录审计日志 | ### 7.4 可扩展性约束 -| 指标 | 要求 | -|------|------| -| 会员数量 | 最多500人 | -| 门店数量 | 单门店 | +| 指标 | 要求 | +| -------- | -------------- | +| 会员数量 | 最多500人 | +| 门店数量 | 单门店 | | 团课容量 | 每节课最多20人 | -| 数据保留 | 保留30天 | +| 数据保留 | 保留30天 | --- @@ -459,16 +510,15 @@ ### 8.1 术语定义 -| 术语 | 定义 | -|------|------| -| 会员 | 在健身房注册的用户 | +| 术语 | 定义 | +| ------ | ------------------------------------------ | +| 会员 | 在健身房注册的用户 | | 会员卡 | 会员购买的权益卡,包括时长卡、次卡、储值卡 | -| 权益 | 会员卡包含的时长、次数、储值、等级等权益 | -| 团课 | 集体课程,由教练带领多个会员一起上课 | -| 预约 | 会员预约团课 | -| 签到 | 会员到店记录 | +| 权益 | 会员卡包含的时长、次数、储值、等级等权益 | +| 团课 | 集体课程,由教练带领多个会员一起上课 | +| 预约 | 会员预约团课 | +| 签到 | 会员到店记录 | ### 8.2 参考文档 - 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001 -- 《健身房管理系统业务概要设计文档》 GYM-HLD-001 diff --git a/docs/design/LLD-基础版系统详细设计.md b/docs/design/LLD-基础版系统详细设计.md index 4040829..152d437 100644 --- a/docs/design/LLD-基础版系统详细设计.md +++ b/docs/design/LLD-基础版系统详细设计.md @@ -20,7 +20,6 @@ - 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001 - 《健身房管理系统基础版业务概要设计文档》 GYM-HLD-BASIC-001 -- 《健身房管理系统详细设计文档》 GYM-LLD-000 - Spring Boot 3 官方文档 - R2DBC 规范文档 - PostgreSQL 官方文档 @@ -874,6 +873,474 @@ public class UserService { --- +### 2.6 UI模版定制模块 + +#### 2.6.1 模块概述 + +UI模版定制模块是基础版的核心基础模块,负责管理租户的UI定制配置,包括: + +- 品牌定制(Logo、颜色、背景图等) +- 布局调整(模块顺序、模块隐藏等) +- 预设模板(模板选择、模板应用等) +- 配置历史(配置回滚、配置对比等) + +#### 2.6.2 数据模型设计 + +**租户UI配置表 (tenant_ui_config)** + +```sql +CREATE TABLE tenant_ui_config ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + version INT NOT NULL DEFAULT 1, + brand_config JSONB NOT NULL DEFAULT '{}', + layout_config JSONB NOT NULL DEFAULT '{}', + template_id BIGINT, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + created_by BIGINT, + updated_by BIGINT, + deleted_at TIMESTAMP DEFAULT NULL, + + CONSTRAINT uk_tenant_ui_config UNIQUE (tenant_id, version), + CONSTRAINT fk_tenant_ui_config_template FOREIGN KEY (template_id) REFERENCES ui_template(id) +); + +CREATE INDEX idx_tenant_ui_config_tenant ON tenant_ui_config(tenant_id); +CREATE INDEX idx_tenant_ui_config_is_active ON tenant_ui_config(is_active); +``` + +**预设模板表 (ui_template)** + +```sql +CREATE TABLE ui_template ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(128) NOT NULL, + code VARCHAR(64) NOT NULL, + type VARCHAR(32) NOT NULL, + description VARCHAR(512), + thumbnail VARCHAR(512), + preview_image VARCHAR(512), + config JSONB NOT NULL DEFAULT '{}', + status SMALLINT DEFAULT 1, + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + created_by BIGINT, + updated_by BIGINT, + deleted_at TIMESTAMP DEFAULT NULL, + + CONSTRAINT uk_ui_template_code UNIQUE (code) +); + +CREATE INDEX idx_ui_template_type ON ui_template(type); +CREATE INDEX idx_ui_template_status ON ui_template(status); +``` + +**配置历史表 (ui_config_history)** + +```sql +CREATE TABLE ui_config_history ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + config_id BIGINT NOT NULL, + version INT NOT NULL, + brand_config JSONB NOT NULL DEFAULT '{}', + layout_config JSONB NOT NULL DEFAULT '{}', + template_id BIGINT, + change_reason VARCHAR(512), + created_at TIMESTAMP DEFAULT NOW(), + created_by BIGINT, + + CONSTRAINT fk_ui_config_history_config FOREIGN KEY (config_id) REFERENCES tenant_ui_config(id), + CONSTRAINT fk_ui_config_history_template FOREIGN KEY (template_id) REFERENCES ui_template(id) +); + +CREATE INDEX idx_ui_config_history_tenant ON ui_config_history(tenant_id); +CREATE INDEX idx_ui_config_history_config ON ui_config_history(config_id); +``` + +**资源文件表 (ui_resource)** + +```sql +CREATE TABLE ui_resource ( + id BIGSERIAL PRIMARY KEY, + tenant_id BIGINT NOT NULL, + resource_type VARCHAR(32) NOT NULL, + resource_name VARCHAR(128) NOT NULL, + file_path VARCHAR(512) NOT NULL, + file_size BIGINT, + file_type VARCHAR(32), + width INT, + height INT, + created_at TIMESTAMP DEFAULT NOW(), + created_by BIGINT, + deleted_at TIMESTAMP DEFAULT NULL, + + CONSTRAINT uk_ui_resource UNIQUE (tenant_id, resource_type, resource_name) +); + +CREATE INDEX idx_ui_resource_tenant ON ui_resource(tenant_id); +CREATE INDEX idx_ui_resource_type ON ui_resource(resource_type); +``` + +#### 2.6.3 核心业务逻辑 + +**品牌配置Service** + +```java +@Service +public class BrandConfigService { + + @Autowired + private TenantUiConfigRepository tenantUiConfigRepository; + + @Autowired + private UiResourceRepository uiResourceRepository; + + @Autowired + private FileStorageService fileStorageService; + + /** + * 上传Logo + */ + @Transactional + public UiResource uploadLogo(Long tenantId, MultipartFile file, Long userId) { + validateLogoFile(file); + + String filePath = fileStorageService.upload(file); + + UiResource resource = new UiResource(); + resource.setTenantId(tenantId); + resource.setResourceType("logo"); + resource.setResourceName(file.getOriginalFilename()); + resource.setFilePath(filePath); + resource.setFileSize(file.getSize()); + resource.setFileType(file.getContentType()); + + BufferedImage image = ImageIO.read(file.getInputStream()); + resource.setWidth(image.getWidth()); + resource.setHeight(image.getHeight()); + + resource = uiResourceRepository.save(resource); + + updateBrandConfig(tenantId, "logo", filePath, userId); + + return resource; + } + + /** + * 验证Logo文件 + */ + private void validateLogoFile(MultipartFile file) { + if (file.getSize() > 2 * 1024 * 1024) { + throw new BusinessException("Logo文件大小不能超过2MB"); + } + + String contentType = file.getContentType(); + if (!"image/png".equals(contentType) && !"image/jpeg".equals(contentType)) { + throw new BusinessException("Logo文件格式只支持PNG或JPG"); + } + } + + /** + * 更新品牌配置 + */ + @Transactional + public void updateBrandConfig(Long tenantId, String key, String value, Long userId) { + TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) + .orElseThrow(() -> new BusinessException("租户配置不存在")); + + JsonObject brandConfig = config.getBrandConfig(); + brandConfig.addProperty(key, value); + + config.setBrandConfig(brandConfig); + config.setUpdatedBy(userId); + + tenantUiConfigRepository.save(config); + } + + /** + * 设置品牌颜色 + */ + @Transactional + public void setBrandColor(Long tenantId, String colorType, String color, Long userId) { + validateColorFormat(color); + + TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) + .orElseThrow(() -> new BusinessException("租户配置不存在")); + + JsonObject brandConfig = config.getBrandConfig(); + brandConfig.addProperty(colorType, color); + + config.setBrandConfig(brandConfig); + config.setUpdatedBy(userId); + + tenantUiConfigRepository.save(config); + } + + /** + * 验证颜色格式 + */ + private void validateColorFormat(String color) { + if (color.matches("^#[0-9A-Fa-f]{6}$")) { + return; + } + + if (color.matches("^rgb\\(\\d{1,3},\\s*\\d{1,3},\\s*\\d{1,3}\\)$")) { + return; + } + + throw new BusinessException("颜色格式不正确,请使用HEX或RGB格式"); + } +} +``` + +**布局配置Service** + +```java +@Service +public class LayoutConfigService { + + @Autowired + private TenantUiConfigRepository tenantUiConfigRepository; + + /** + * 更新模块顺序 + */ + @Transactional + public void updateModuleOrder(Long tenantId, List moduleOrder, Long userId) { + TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) + .orElseThrow(() -> new BusinessException("租户配置不存在")); + + JsonObject layoutConfig = config.getLayoutConfig(); + JsonArray moduleOrderArray = new JsonArray(); + moduleOrder.forEach(moduleOrderArray::add); + + layoutConfig.add("moduleOrder", moduleOrderArray); + + config.setLayoutConfig(layoutConfig); + config.setUpdatedBy(userId); + + tenantUiConfigRepository.save(config); + } + + /** + * 隐藏/显示模块 + */ + @Transactional + public void toggleModuleVisibility(Long tenantId, String moduleCode, boolean visible, Long userId) { + TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) + .orElseThrow(() -> new BusinessException("租户配置不存在")); + + JsonObject layoutConfig = config.getLayoutConfig(); + JsonArray hiddenModules = layoutConfig.has("hiddenModules") + ? layoutConfig.getAsJsonArray("hiddenModules") + : new JsonArray(); + + if (visible) { + hiddenModules.removeIf(element -> element.getAsString().equals(moduleCode)); + } else { + if (!hiddenModules.contains(new JsonPrimitive(moduleCode))) { + hiddenModules.add(moduleCode); + } + } + + layoutConfig.add("hiddenModules", hiddenModules); + + config.setLayoutConfig(layoutConfig); + config.setUpdatedBy(userId); + + tenantUiConfigRepository.save(config); + } + + /** + * 设置首页布局类型 + */ + @Transactional + public void setHomeLayoutType(Long tenantId, String layoutType, Long userId) { + TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) + .orElseThrow(() -> new BusinessException("租户配置不存在")); + + JsonObject layoutConfig = config.getLayoutConfig(); + layoutConfig.addProperty("homeLayoutType", layoutType); + + config.setLayoutConfig(layoutConfig); + config.setUpdatedBy(userId); + + tenantUiConfigRepository.save(config); + } +} +``` + +**模板管理Service** + +```java +@Service +public class TemplateService { + + @Autowired + private UiTemplateRepository uiTemplateRepository; + + @Autowired + private TenantUiConfigRepository tenantUiConfigRepository; + + @Autowired + private UiConfigHistoryRepository uiConfigHistoryRepository; + + /** + * 获取所有可用模板 + */ + public List getAvailableTemplates() { + return uiTemplateRepository.findByStatus(1); + } + + /** + * 应用模板 + */ + @Transactional + public void applyTemplate(Long tenantId, Long templateId, Long userId) { + UiTemplate template = uiTemplateRepository.findById(templateId) + .orElseThrow(() -> new BusinessException("模板不存在")); + + if (template.getStatus() != 1) { + throw new BusinessException("模板不可用"); + } + + TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) + .orElseThrow(() -> new BusinessException("租户配置不存在")); + + saveConfigToHistory(config); + + JsonObject templateConfig = template.getConfig(); + JsonObject layoutConfig = templateConfig.getAsJsonObject("layoutConfig"); + + config.setLayoutConfig(layoutConfig); + config.setTemplateId(templateId); + config.setUpdatedBy(userId); + + tenantUiConfigRepository.save(config); + } + + /** + * 保存配置到历史 + */ + private void saveConfigToHistory(TenantUiConfig config) { + UiConfigHistory history = new UiConfigHistory(); + history.setTenantId(config.getTenantId()); + history.setConfigId(config.getId()); + history.setVersion(config.getVersion()); + history.setBrandConfig(config.getBrandConfig()); + history.setLayoutConfig(config.getLayoutConfig()); + history.setTemplateId(config.getTemplateId()); + + uiConfigHistoryRepository.save(history); + } + + /** + * 获取模板详情 + */ + public UiTemplate getTemplateDetail(Long templateId) { + return uiTemplateRepository.findById(templateId) + .orElseThrow(() -> new BusinessException("模板不存在")); + } +} +``` + +**配置历史Service** + +```java +@Service +public class ConfigHistoryService { + + @Autowired + private UiConfigHistoryRepository uiConfigHistoryRepository; + + @Autowired + private TenantUiConfigRepository tenantUiConfigRepository; + + /** + * 获取配置历史列表 + */ + public List getConfigHistory(Long tenantId) { + return uiConfigHistoryRepository.findByTenantIdOrderByCreatedAtDesc(tenantId); + } + + /** + * 回滚到历史版本 + */ + @Transactional + public void rollbackToVersion(Long tenantId, Long historyId, Long userId) { + UiConfigHistory history = uiConfigHistoryRepository.findById(historyId) + .orElseThrow(() -> new BusinessException("历史版本不存在")); + + if (!history.getTenantId().equals(tenantId)) { + throw new BusinessException("无权访问该历史版本"); + } + + TenantUiConfig config = tenantUiConfigRepository.findActiveByTenantId(tenantId) + .orElseThrow(() -> new BusinessException("租户配置不存在")); + + config.setBrandConfig(history.getBrandConfig()); + config.setLayoutConfig(history.getLayoutConfig()); + config.setTemplateId(history.getTemplateId()); + config.setVersion(config.getVersion() + 1); + config.setUpdatedBy(userId); + + tenantUiConfigRepository.save(config); + } + + /** + * 对比配置 + */ + public Map compareConfigs(Long tenantId, Long historyId) { + TenantUiConfig currentConfig = tenantUiConfigRepository.findActiveByTenantId(tenantId) + .orElseThrow(() -> new BusinessException("租户配置不存在")); + + UiConfigHistory historyConfig = uiConfigHistoryRepository.findById(historyId) + .orElseThrow(() -> new BusinessException("历史版本不存在")); + + Map diff = new HashMap<>(); + diff.put("brandConfigDiff", compareJsonObjects( + currentConfig.getBrandConfig(), + historyConfig.getBrandConfig() + )); + diff.put("layoutConfigDiff", compareJsonObjects( + currentConfig.getLayoutConfig(), + historyConfig.getLayoutConfig() + )); + + return diff; + } + + /** + * 比较JSON对象 + */ + private Map compareJsonObjects(JsonObject obj1, JsonObject obj2) { + Map diff = new HashMap<>(); + + Set allKeys = new HashSet<>(); + obj1.keySet().forEach(allKeys::add); + obj2.keySet().forEach(allKeys::add); + + for (String key : allKeys) { + if (!obj1.has(key)) { + diff.put(key, "新增: " + obj2.get(key)); + } else if (!obj2.has(key)) { + diff.put(key, "删除: " + obj1.get(key)); + } else if (!obj1.get(key).equals(obj2.get(key))) { + diff.put(key, "修改: " + obj1.get(key) + " -> " + obj2.get(key)); + } + } + + return diff; + } +} +``` + +--- + ## 三、API设计 ### 3.1 会员模块API @@ -1028,6 +1495,273 @@ Response: --- +### 3.5 UI模版定制模块API + +#### 3.5.1 上传Logo + +``` +POST /api/v1/ui-config/logo +Content-Type: multipart/form-data + +Request: +{ + "file": , + "tenantId": 1 +} + +Response: +{ + "code": 200, + "message": "上传成功", + "data": { + "id": 1, + "resourceType": "logo", + "resourceName": "logo.png", + "filePath": "/uploads/logo/1/logo.png", + "fileSize": 102400, + "fileType": "image/png", + "width": 200, + "height": 200, + "createdAt": "2026-03-07T10:00:00Z" + } +} +``` + +#### 3.5.2 设置品牌颜色 + +``` +POST /api/v1/ui-config/brand/color + +Request: +{ + "tenantId": 1, + "colorType": "primaryColor", + "color": "#FF5733" +} + +Response: +{ + "code": 200, + "message": "设置成功", + "data": { + "primaryColor": "#FF5733", + "secondaryColor": "#FFC300", + "updatedAt": "2026-03-07T10:00:00Z" + } +} +``` + +#### 3.5.3 更新模块顺序 + +``` +POST /api/v1/ui-config/layout/module-order + +Request: +{ + "tenantId": 1, + "moduleOrder": ["dashboard", "member", "booking", "checkin", "statistics"] +} + +Response: +{ + "code": 200, + "message": "更新成功", + "data": { + "moduleOrder": ["dashboard", "member", "booking", "checkin", "statistics"], + "updatedAt": "2026-03-07T10:00:00Z" + } +} +``` + +#### 3.5.4 隐藏/显示模块 + +``` +POST /api/v1/ui-config/layout/module-visibility + +Request: +{ + "tenantId": 1, + "moduleCode": "statistics", + "visible": false +} + +Response: +{ + "code": 200, + "message": "更新成功", + "data": { + "hiddenModules": ["statistics"], + "updatedAt": "2026-03-07T10:00:00Z" + } +} +``` + +#### 3.5.5 获取可用模板列表 + +``` +GET /api/v1/ui-config/templates + +Response: +{ + "code": 200, + "message": "查询成功", + "data": [ + { + "id": 1, + "name": "简约风格", + "code": "simple", + "type": "简约", + "description": "简洁清爽的设计风格", + "thumbnail": "/templates/simple/thumbnail.png", + "previewImage": "/templates/simple/preview.png", + "sortOrder": 1 + }, + { + "id": 2, + "name": "运动风格", + "code": "sport", + "type": "运动", + "description": "活力四射的运动风格", + "thumbnail": "/templates/sport/thumbnail.png", + "previewImage": "/templates/sport/preview.png", + "sortOrder": 2 + } + ] +} +``` + +#### 3.5.6 应用模板 + +``` +POST /api/v1/ui-config/template/apply + +Request: +{ + "tenantId": 1, + "templateId": 1 +} + +Response: +{ + "code": 200, + "message": "应用成功", + "data": { + "templateId": 1, + "templateName": "简约风格", + "appliedAt": "2026-03-07T10:00:00Z" + } +} +``` + +#### 3.5.7 获取配置历史 + +``` +GET /api/v1/ui-config/history?tenantId=1 + +Response: +{ + "code": 200, + "message": "查询成功", + "data": [ + { + "id": 1, + "version": 1, + "templateId": 1, + "changeReason": "应用简约风格模板", + "createdAt": "2026-03-07T10:00:00Z", + "createdBy": 1 + }, + { + "id": 2, + "version": 2, + "templateId": 2, + "changeReason": "切换到运动风格模板", + "createdAt": "2026-03-07T11:00:00Z", + "createdBy": 1 + } + ] +} +``` + +#### 3.5.8 回滚到历史版本 + +``` +POST /api/v1/ui-config/history/rollback + +Request: +{ + "tenantId": 1, + "historyId": 1 +} + +Response: +{ + "code": 200, + "message": "回滚成功", + "data": { + "version": 3, + "rolledBackFrom": 1, + "rolledBackAt": "2026-03-07T12:00:00Z" + } +} +``` + +#### 3.5.9 对比配置 + +``` +GET /api/v1/ui-config/history/compare?tenantId=1&historyId=1 + +Response: +{ + "code": 200, + "message": "查询成功", + "data": { + "brandConfigDiff": { + "primaryColor": "修改: #FF5733 -> #FFC300", + "logo": "删除: /uploads/logo/1/logo.png" + }, + "layoutConfigDiff": { + "moduleOrder": "修改: [\"dashboard\", \"member\"] -> [\"member\", \"dashboard\"]", + "homeLayoutType": "新增: card" + } + } +} +``` + +#### 3.5.10 获取当前配置 + +``` +GET /api/v1/ui-config/current?tenantId=1 + +Response: +{ + "code": 200, + "message": "查询成功", + "data": { + "id": 1, + "tenantId": 1, + "version": 3, + "brandConfig": { + "logo": "/uploads/logo/1/logo.png", + "primaryColor": "#FF5733", + "secondaryColor": "#FFC300", + "brandName": "我的健身房", + "slogan": "健康生活,从现在开始" + }, + "layoutConfig": { + "moduleOrder": ["dashboard", "member", "booking", "checkin"], + "hiddenModules": ["statistics"], + "homeLayoutType": "card" + }, + "templateId": 1, + "isActive": true, + "updatedAt": "2026-03-07T10:00:00Z" + } +} +``` + +--- + ## 四、缓存策略 ### 4.1 缓存设计 @@ -1319,7 +2053,7 @@ public class GlobalExceptionHandler { | 系统指标 | 磁盘使用率 | ≤ 80% | | 应用指标 | API响应时间 | ≤ 500ms | | 应用指标 | 错误率 | ≤ 1% | -| 应用指标 | 并发数 | ≤ 100 | +| 应用指标 | 并发用户数 | ≤ 100 | --- @@ -1340,7 +2074,6 @@ public class GlobalExceptionHandler { - 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001 - 《健身房管理系统基础版业务概要设计文档》 GYM-HLD-BASIC-001 -- 《健身房管理系统详细设计文档》 GYM-LLD-000 - Spring Boot 3 官方文档 - R2DBC 规范文档 - PostgreSQL 官方文档 diff --git a/docs/product/PRD-基础版产品设计文档.md b/docs/product/PRD-基础版产品设计文档.md index 16ec809..fc77190 100644 --- a/docs/product/PRD-基础版产品设计文档.md +++ b/docs/product/PRD-基础版产品设计文档.md @@ -275,6 +275,137 @@ --- +### 2.6 UI模版定制模块 + +#### 2.6.1 品牌定制 + +**功能描述**:租户通过可视化配置器定制品牌元素,包括Logo、颜色、背景图等。 + +**用户故事**:作为一个租户,我希望能够上传自己的Logo和设置品牌颜色,以便在系统中展示我的品牌特色。 + +**功能点**: +- Logo上传(支持拖拽上传、自动裁剪、多尺寸缩略图) +- 品牌主色调设置(颜色选择器、预设色板) +- 品牌辅助色设置 +- 背景图上传(支持轮播背景) +- 品牌名称和Slogan设置 +- 实时预览所有品牌元素 + +**业务规则**: +- Logo支持PNG/JPG格式,限制2MB以内 +- 颜色支持RGB和HEX格式 +- 品牌元素应用范围包括小程序和管理后台 +- 配置变更实时生效,无需重新部署 + +**验收标准**: +- Logo上传成功率 ≥ 95% +- 实时预览响应时间 ≤ 200ms +- 品牌元素应用一致性 100% + +#### 2.6.2 布局调整 + +**功能描述**:租户通过拖拽式界面调整页面模块的显示顺序和布局结构。 + +**用户故事**:作为一个租户,我希望能够调整页面的模块顺序和隐藏不需要的功能,以便优化用户体验。 + +**功能点**: +- 模块顺序调整(拖拽排序) +- 模块隐藏/显示开关 +- 首页布局类型选择(卡片式、列表式、轮播式) +- 导航菜单自定义(添加/编辑/删除菜单项) +- 模块分组管理 +- 批量操作(全选、反选、批量隐藏) +- 布局调整撤销/重做 + +**业务规则**: +- 模块顺序调整支持跨区域移动 +- 隐藏的模块不显示但数据保留 +- 布局调整按角色区分(店长、前台、会员) +- 布局变更自动保存到配置历史 + +**验收标准**: +- 拖拽操作流畅度 ≥ 90% +- 布局变更响应时间 ≤ 300ms +- 模块隐藏成功率 100% + +#### 2.6.3 预设模板 + +**功能描述**:系统提供3-5个精心设计的预设模板,租户可以直接选择并应用。 + +**用户故事**:作为一个租户,我希望能够从预设模板中选择适合我的模板,快速完成UI定制。 + +**功能点**: +- 模板预览(缩略图、大图预览) +- 模板类型筛选(简约、运动、科技、高端) +- 一键应用模板 +- 模板收藏功能 +- 模板对比功能(并排对比、差异高亮) +- 模板应用前确认对话框 +- 模板预览模式(正式应用前预览效果) + +**业务规则**: +- 模板应用后保留租户已有的品牌配置 +- 模板支持版本控制和灰度发布 +- 模板切换支持配置合并 +- 禁用的模板不可选择 + +**验收标准**: +- 模板加载成功率 ≥ 98% +- 模板应用成功率 ≥ 95% +- 模板切换响应时间 ≤ 500ms + +#### 2.6.4 配置历史 + +**功能描述**:记录租户的配置变更历史,支持配置回滚和对比。 + +**用户故事**:作为一个租户,我希望能够查看配置变更历史,并在需要时回滚到之前的配置。 + +**功能点**: +- 配置历史列表查看 +- 配置版本对比(新旧配置差异) +- 配置回滚到历史版本 +- 配置导出(JSON文件) +- 配置导入(从JSON文件恢复) +- 变更原因记录 + +**业务规则**: +- 每次配置变更自动生成新版本号 +- 配置历史保留90天 +- 回滚操作需要确认 +- 配置对比高亮显示差异 + +**验收标准**: +- 配置保存成功率 ≥ 99% +- 配置回滚成功率 ≥ 98% +- 配置对比准确性 100% + +#### 2.6.5 可视化配置器 + +**功能描述**:提供直观的可视化配置界面,降低租户定制UI的技术门槛。 + +**用户故事**:作为一个租户,我希望通过可视化的拖拽界面来定制UI,而不需要编写代码。 + +**功能点**: +- 三区域布局(品牌配置区、布局配置区、模板选择区) +- 拖拽式模块排序 +- 实时预览(支持多设备尺寸切换) +- 智能提示(颜色搭配建议、Logo尺寸建议、模板推荐) +- 快捷操作(一键重置、一键预览、一键保存、一键发布) +- 配置导出/导入 + +**业务规则**: +- 所有配置变更实时反映在预览区 +- 预览区模拟真实页面结构 +- 拖拽操作提供视觉反馈 +- 配置器支持键盘快捷键 + +**验收标准**: +- 配置器加载时间 ≤ 1秒 +- 实时预览延迟 ≤ 100ms +- 拖拽操作流畅度 ≥ 95% + +--- + ## 三、非功能需求 ### 3.1 性能需求 @@ -381,6 +512,5 @@ ### 7.2 参考文档 -- 《健身房管理系统产品设计文档》 GYM-PRD-001 -- 《健身房管理系统业务概要设计文档》 GYM-HLD-001 -- 《健身房管理系统详细设计文档》 GYM-LLD-000 +- 《健身房管理系统基础版业务概要设计文档》 GYM-HLD-BASIC-001 +- 《健身房管理系统基础版详细设计文档》 GYM-LLD-BASIC-001