feat: 同步UI模版定制功能到PRD、HLD、LLD文档

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模版定制功能已完整同步到产品需求、概要设计、详细设计文档中
This commit is contained in:
张翔
2026-03-07 16:59:32 +08:00
parent 559bfe56e3
commit 971d51cb36
3 changed files with 1011 additions and 98 deletions
+736 -3
View File
@@ -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<String> 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<UiTemplate> 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<UiConfigHistory> 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<String, Object> compareConfigs(Long tenantId, Long historyId) {
TenantUiConfig currentConfig = tenantUiConfigRepository.findActiveByTenantId(tenantId)
.orElseThrow(() -> new BusinessException("租户配置不存在"));
UiConfigHistory historyConfig = uiConfigHistoryRepository.findById(historyId)
.orElseThrow(() -> new BusinessException("历史版本不存在"));
Map<String, Object> diff = new HashMap<>();
diff.put("brandConfigDiff", compareJsonObjects(
currentConfig.getBrandConfig(),
historyConfig.getBrandConfig()
));
diff.put("layoutConfigDiff", compareJsonObjects(
currentConfig.getLayoutConfig(),
historyConfig.getLayoutConfig()
));
return diff;
}
/**
* 比较JSON对象
*/
private Map<String, Object> compareJsonObjects(JsonObject obj1, JsonObject obj2) {
Map<String, Object> diff = new HashMap<>();
Set<String> 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": <binary>,
"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 官方文档