Compare commits

...

2 Commits

Author SHA1 Message Date
16f1d653c3 feat(tenant): 新增代理租户创建与分成比例校验 2025-12-25 19:17:50 +08:00
5c95baf336 feat(userthemes): 新增用户主题管理功能
新增用户主题完整CRUD模块,含控制器、DO、Mapper、Service及VO定义,并补充错误码。
2025-12-25 15:24:32 +08:00
7 changed files with 147 additions and 23 deletions

View File

@@ -67,6 +67,7 @@ The project uses a **modular monolith** architecture with the following key modu
- `yolo-spring-boot-starter-biz-*`: Business-specific components (tenant, data permission, IP) - `yolo-spring-boot-starter-biz-*`: Business-specific components (tenant, data permission, IP)
- **yolo-module-system**: System management module (users, roles, permissions, dictionaries) - **yolo-module-system**: System management module (users, roles, permissions, dictionaries)
- **yolo-module-infra**: Infrastructure module (jobs, file storage, code generator, monitoring) - **yolo-module-infra**: Infrastructure module (jobs, file storage, code generator, monitoring)
- **keyboard-server**: Business module for keyboard-specific features (custom controllers, services, DAL)
- **yolo-server**: Main application container (empty shell that aggregates modules) - **yolo-server**: Main application container (empty shell that aggregates modules)
### Layered Architecture ### Layered Architecture
@@ -104,30 +105,17 @@ enums/ # Module-specific enums
- Connection: `jdbc:postgresql://localhost:5432/keyborad_db` - Connection: `jdbc:postgresql://localhost:5432/keyborad_db`
- Default credentials: root/123asd - Default credentials: root/123asd
### Database Scripts
SQL scripts are located in `sql/` directory with support for multiple databases:
- `sql/postgresql/` - PostgreSQL scripts
- `sql/mysql/` - MySQL scripts
- `sql/oracle/`, `sql/sqlserver/`, `sql/dm/`, `sql/kingbase/`, `sql/opengauss/` - Other DB support
### Database Conversion
Use `sql/tools/convertor.py` to convert MySQL scripts to other databases:
```bash
cd sql/tools
python3 convertor.py postgres > output.sql
```
### Quick Database Setup with Docker ### Quick Database Setup with Docker
```bash ```bash
cd sql/tools cd script/docker
docker compose up -d postgres docker compose up -d
``` ```
## Configuration ## Configuration
### Application Profiles ### Application Profiles
- `application.yaml` - Base configuration - `application.yaml` - Base configuration
- `application-local.yaml` - Local development (port 48080) - `application-local.yaml` - Local development (port 48081)
- `application-dev.yaml` - Development environment - `application-dev.yaml` - Development environment
### Key Configuration Properties ### Key Configuration Properties
@@ -174,15 +162,15 @@ Configured via `spring.datasource.dynamic.datasource`:
## API Documentation ## API Documentation
- **Swagger UI**: http://localhost:48080/swagger-ui - **Swagger UI**: http://localhost:48081/swagger-ui
- **Knife4j UI**: http://localhost:48080/doc.html (enhanced Swagger UI) - **Knife4j UI**: http://localhost:48081/doc.html (enhanced Swagger UI)
- **OpenAPI JSON**: http://localhost:48080/v3/api-docs - **OpenAPI JSON**: http://localhost:48081/v3/api-docs
## Monitoring & Admin ## Monitoring & Admin
- **Spring Boot Admin**: http://localhost:48080/admin - **Spring Boot Admin**: http://localhost:48081/admin
- **Actuator**: http://localhost:48080/actuator - **Actuator**: http://localhost:48081/actuator
- **Druid Monitor**: http://localhost:48080/druid (database connection pool) - **Druid Monitor**: http://localhost:48081/druid (database connection pool)
## Common Issues ## Common Issues
@@ -193,7 +181,7 @@ If you encounter startup issues, refer to: https://doc.iocoder.cn/quick-start/
Ensure PostgreSQL is running and credentials in `application-local.yaml` are correct. Ensure PostgreSQL is running and credentials in `application-local.yaml` are correct.
### Port Conflicts ### Port Conflicts
Default port is 48080. Change via `server.port` in configuration files. Default port is 48081. Change via `server.port` in configuration files.
## Dependencies & Versions ## Dependencies & Versions

View File

@@ -7,6 +7,7 @@ import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import static com.yolo.keyboard.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; import static com.yolo.keyboard.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@@ -33,4 +34,35 @@ public class TenantPageReqVO extends PageParam {
@Schema(description = "创建时间") @Schema(description = "创建时间")
private LocalDateTime[] createTime; private LocalDateTime[] createTime;
/**
* 上级租户 Id
*/
@Schema(description = "上级租户编号")
private Long parentId;
/**
* 备注
*/
@Schema(description = "备注")
private String remark;
/**
* 租户类型
*/
@Schema(description = "租户类型")
private String tenantType;
/**
* 代理级别
*/
@Schema(description = "代理级别")
private Integer tenantLevel;
/**
* 分润比例
*/
@Schema(description = "分润比例")
private BigDecimal profitShareRatio;
} }

View File

@@ -8,6 +8,7 @@ import com.yolo.keyboard.module.system.enums.DictTypeConstants;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@@ -53,4 +54,32 @@ public class TenantRespVO {
@ExcelProperty("创建时间") @ExcelProperty("创建时间")
private LocalDateTime createTime; private LocalDateTime createTime;
/**
* 上级租户 Id
*/
@Schema(description = "上级租户编号")
private Long parentId;
/**
* 备注
*/
@Schema(description = "备注")
private String remark;
/**
* 租户类型
*/
@Schema(description = "租户类型")
private String tenantType;
/**
* 代理级别
*/
@Schema(description = "代理级别")
private Integer tenantLevel;
@Schema(description = "分润比例")
private BigDecimal profitShareRatio;
} }

View File

@@ -10,6 +10,7 @@ import jakarta.validation.constraints.Size;
import lombok.Data; import lombok.Data;
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Length;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@@ -68,4 +69,32 @@ public class TenantSaveReqVO {
|| (ObjectUtil.isAllNotEmpty(username, password)); // 新增时,必须都传递 username、password || (ObjectUtil.isAllNotEmpty(username, password)); // 新增时,必须都传递 username、password
} }
/**
* 上级租户 Id
*/
@Schema(description = "上级租户编号")
private Long parentId;
/**
* 备注
*/
@Schema(description = "备注")
private String remark;
/**
* 租户类型
*/
@Schema(description = "租户类型")
private String tenantType;
/**
* 代理级别
*/
@Schema(description = "代理级别")
private Integer tenantLevel;
@Schema(description = "分润比例")
private BigDecimal profitShareRatio;
} }

View File

@@ -8,8 +8,10 @@ import com.yolo.keyboard.module.system.dal.dataobject.user.AdminUserDO;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*; import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@@ -86,4 +88,27 @@ public class TenantDO extends BaseDO {
*/ */
private Integer accountCount; private Integer accountCount;
/**
* 上级租户 Id
*/
private Long parentId;
/**
* 备注
*/
private String remark;
/**
* 租户类型
*/
private String tenantType;
/**
* 代理级别
*/
private Integer tenantLevel;
private BigDecimal profitShareRatio;
} }

View File

@@ -109,6 +109,8 @@ public interface ErrorCodeConstants {
ErrorCode TENANT_CAN_NOT_UPDATE_SYSTEM = new ErrorCode(1_002_015_003, "系统租户不能进行修改、删除等操作!"); ErrorCode TENANT_CAN_NOT_UPDATE_SYSTEM = new ErrorCode(1_002_015_003, "系统租户不能进行修改、删除等操作!");
ErrorCode TENANT_NAME_DUPLICATE = new ErrorCode(1_002_015_004, "名字为【{}】的租户已存在"); ErrorCode TENANT_NAME_DUPLICATE = new ErrorCode(1_002_015_004, "名字为【{}】的租户已存在");
ErrorCode TENANT_WEBSITE_DUPLICATE = new ErrorCode(1_002_015_005, "域名为【{}】的租户已存在"); ErrorCode TENANT_WEBSITE_DUPLICATE = new ErrorCode(1_002_015_005, "域名为【{}】的租户已存在");
ErrorCode TENANT_LEVEL_MAX = new ErrorCode(1_002_015_006, "当前租户级别已达到最大值,不允许创建代理租户");
ErrorCode TENANT_PROFIT_SHARE_RATIO_INVALID = new ErrorCode(1_002_015_007, "分成比例必须在 0 到 1 之间");
// ========== 租户套餐 1-002-016-000 ========== // ========== 租户套餐 1-002-016-000 ==========
ErrorCode TENANT_PACKAGE_NOT_EXISTS = new ErrorCode(1_002_016_000, "租户套餐不存在"); ErrorCode TENANT_PACKAGE_NOT_EXISTS = new ErrorCode(1_002_016_000, "租户套餐不存在");

View File

@@ -37,6 +37,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@@ -104,9 +105,27 @@ public class TenantServiceImpl implements TenantService {
validTenantWebsiteDuplicate(createReqVO.getWebsites(), null); validTenantWebsiteDuplicate(createReqVO.getWebsites(), null);
// 校验套餐被禁用 // 校验套餐被禁用
TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId()); TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId());
// 校验分成比例
if (createReqVO.getProfitShareRatio() != null &&
(createReqVO.getProfitShareRatio().compareTo(BigDecimal.ZERO) < 0 ||
createReqVO.getProfitShareRatio().compareTo(BigDecimal.ONE) > 0)) {
throw exception(TENANT_PROFIT_SHARE_RATIO_INVALID);
}
// 创建租户 // 创建租户
TenantDO tenant = BeanUtils.toBean(createReqVO, TenantDO.class); TenantDO tenant = BeanUtils.toBean(createReqVO, TenantDO.class);
// 处理代理租户逻辑
if ("代理".equals(createReqVO.getTenantType())) {
Long currentTenantId = TenantContextHolder.getTenantId();
if (currentTenantId != null) {
TenantDO parentTenant = tenantMapper.selectById(currentTenantId);
if (parentTenant != null && parentTenant.getTenantLevel() != null && parentTenant.getTenantLevel() == 2) {
throw exception(TENANT_LEVEL_MAX);
}
tenant.setParentId(currentTenantId);
tenant.setTenantLevel(parentTenant != null && parentTenant.getTenantLevel() != null ? parentTenant.getTenantLevel() + 1 : 1);
}
}
tenantMapper.insert(tenant); tenantMapper.insert(tenant);
// 创建租户的管理员 // 创建租户的管理员
TenantUtils.execute(tenant.getId(), () -> { TenantUtils.execute(tenant.getId(), () -> {