refactor(entity): 中间表优化 - 移除联合唯一索引和 deleted 字段
- CourseCollectionPackage、TenantCourse、ClassTeacher 改为独立实体类 - 不再继承 BaseEntity,移除 deleted 字段,使用物理删除 - 创建 V41 迁移脚本移除联合唯一索引和 deleted 字段 - 更新中间表设计规范到 CLAUDE.md 和统一开发规范.md refactor(entity): 中间表优化 - 移除联合唯一索引和 deleted 字段 - CourseCollectionPackage、TenantCourse、ClassTeacher 改为独立实体类 - 不再继承 BaseEntity,移除 deleted 字段,使用物理删除 - 创建 V41 迁移脚本移除联合唯一索引和 deleted 字段 - 更新中间表设计规范到 CLAUDE.md 和统一开发规范.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7743ae7a01
commit
60213b513d
@ -839,3 +839,71 @@ DRAFT(草稿)→ 提交审核 → PENDING(待审核)→ 审核通过 →
|
|||||||
- **APPROVED**: 可发布
|
- **APPROVED**: 可发布
|
||||||
- **PUBLISHED**: 可下架
|
- **PUBLISHED**: 可下架
|
||||||
- **OFFLINE**: 终止状态
|
- **OFFLINE**: 终止状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 中间表设计规范(2026-03-19)
|
||||||
|
|
||||||
|
### 核心原则
|
||||||
|
|
||||||
|
**中间表(关联表)不使用逻辑删除,使用物理删除,不设置联合唯一索引。**
|
||||||
|
|
||||||
|
### 表类型分类
|
||||||
|
|
||||||
|
| 表类型 | 特点 | 删除策略 | 唯一索引 | 示例表 |
|
||||||
|
|--------|------|----------|----------|--------|
|
||||||
|
| **A 类:纯关联表(中间表)** | 只表示 A 和 B 的关系,无额外状态,先删后增的更新模式 | **物理删除**(不设 deleted) | **不设置**联合唯一索引 | `course_collection_package`、`tenant_course`、`class_teacher` |
|
||||||
|
| **B 类:关系表** | 关系相对稳定,有业务历史价值 | 逻辑删除 | 建议设置联合唯一索引 | `parent_student` |
|
||||||
|
| **C 类:配置表** | (key, value) 结构,天然应该唯一 | 逻辑删除 | **设置**联合唯一索引 | `system_setting` |
|
||||||
|
| **D 类:合同/授权表** | 有合同性质,需要保留历史 | 逻辑删除 | 不设置联合唯一索引 | `tenant_package` |
|
||||||
|
|
||||||
|
### A 类表实体类写法
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 中间表,不设置逻辑删除字段,使用物理删除
|
||||||
|
* 不继承 BaseEntity,因此没有 deleted 字段
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("course_collection_package")
|
||||||
|
public class CourseCollectionPackage {
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
private Long collectionId;
|
||||||
|
private Long packageId;
|
||||||
|
private Integer sortOrder;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service 层操作示例
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 更新套餐包含的课程包(先删后增)
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updatePackages(Long collectionId, List<Long> packageIds) {
|
||||||
|
// 1. 去重
|
||||||
|
Set<Long> uniquePackageIds = new LinkedHashSet<>(packageIds);
|
||||||
|
|
||||||
|
// 2. 物理删除所有关联
|
||||||
|
mapper.delete(new LambdaQueryWrapper<CourseCollectionPackage>()
|
||||||
|
.eq(CourseCollectionPackage::getCollectionId, collectionId));
|
||||||
|
|
||||||
|
// 3. 批量插入
|
||||||
|
List<CourseCollectionPackage> records = new ArrayList<>();
|
||||||
|
int sortOrder = 0;
|
||||||
|
for (Long packageId : uniquePackageIds) {
|
||||||
|
CourseCollectionPackage record = new CourseCollectionPackage();
|
||||||
|
record.setCollectionId(collectionId);
|
||||||
|
record.setPackageId(packageId);
|
||||||
|
record.setSortOrder(sortOrder++);
|
||||||
|
records.add(record);
|
||||||
|
}
|
||||||
|
saveBatch(records);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库迁移
|
||||||
|
|
||||||
|
- `V41__optimize_association_tables.sql` - 移除中间表的联合唯一索引和 deleted 字段
|
||||||
|
|||||||
225
docs/统一开发规范.md
225
docs/统一开发规范.md
@ -3484,3 +3484,228 @@ public class RateLimiter {
|
|||||||
2. **异常情况放行**:当 Redis 不可用时,建议放行请求,避免影响正常业务
|
2. **异常情况放行**:当 Redis 不可用时,建议放行请求,避免影响正常业务
|
||||||
3. **集群部署**:滑动窗口算法天然支持分布式,无需额外配置
|
3. **集群部署**:滑动窗口算法天然支持分布式,无需额外配置
|
||||||
4. **监控告警**:建议添加限流触发的监控告警,及时发现异常流量
|
4. **监控告警**:建议添加限流触发的监控告警,及时发现异常流量
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十三、中间表设计规范(逻辑删除与唯一索引)
|
||||||
|
|
||||||
|
### 核心原则
|
||||||
|
|
||||||
|
**中间表(关联表)不使用逻辑删除,使用物理删除,不设置联合唯一索引。**
|
||||||
|
|
||||||
|
### 表类型分类
|
||||||
|
|
||||||
|
| 表类型 | 特点 | 删除策略 | 唯一索引 | 示例表 |
|
||||||
|
|--------|------|----------|----------|--------|
|
||||||
|
| **A 类:纯关联表(中间表)** | 只表示 A 和 B 的关系,无额外状态,先删后增的更新模式 | **物理删除**(不设 deleted) | **不设置**联合唯一索引 | `course_collection_package`、`tenant_course`、`class_teacher` |
|
||||||
|
| **B 类:关系表** | 关系相对稳定,有业务历史价值 | 逻辑删除 | 建议设置联合唯一索引 | `parent_student` |
|
||||||
|
| **C 类:配置表** | (key, value) 结构,天然应该唯一 | 逻辑删除 | **设置**联合唯一索引 | `system_setting` |
|
||||||
|
| **D 类:合同/授权表** | 有合同性质,需要保留历史 | 逻辑删除 | 不设置联合唯一索引 | `tenant_package` |
|
||||||
|
|
||||||
|
### A 类表(纯关联表)设计规范
|
||||||
|
|
||||||
|
#### 1. 为什么不使用逻辑删除?
|
||||||
|
|
||||||
|
**原因**:
|
||||||
|
- 中间表无业务历史价值,删除后不需要保留记录
|
||||||
|
- 逻辑删除会导致数据无限膨胀(每次删除都保留)
|
||||||
|
- 与唯一索引配合使用时会产生冲突(同一组合反复删除会冲突)
|
||||||
|
- 物理删除更符合业务直觉,代码更简洁
|
||||||
|
|
||||||
|
#### 2. 为什么不设置联合唯一索引?
|
||||||
|
|
||||||
|
**原因**:
|
||||||
|
- 单体应用 + 低并发场景下,并发冲突概率极低
|
||||||
|
- 应用层校验(保存前去重、添加前检查)足够可靠
|
||||||
|
- 批量操作时,有索引会导致一条失败全回滚,体验差
|
||||||
|
- 先删后增的更新模式下,唯一索引无实际意义
|
||||||
|
|
||||||
|
**风险可控**:
|
||||||
|
- 应用层有完善的校验逻辑
|
||||||
|
- 定时任务检查脏数据
|
||||||
|
- 接受极端情况下的脏数据风险(概率极低)
|
||||||
|
|
||||||
|
#### 3. 实体类写法
|
||||||
|
|
||||||
|
```java
|
||||||
|
package com.reading.platform.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程套餐与课程包关联实体
|
||||||
|
* <p>
|
||||||
|
* 中间表,不设置逻辑删除字段,使用物理删除
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
@Schema(description = "课程套餐与课程包关联实体")
|
||||||
|
@Data
|
||||||
|
@TableName("course_collection_package")
|
||||||
|
public class CourseCollectionPackage {
|
||||||
|
|
||||||
|
@Schema(description = "主键 ID")
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "课程套餐 ID")
|
||||||
|
private Long collectionId;
|
||||||
|
|
||||||
|
@Schema(description = "课程包 ID")
|
||||||
|
private Long packageId;
|
||||||
|
|
||||||
|
@Schema(description = "排序号")
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- **不继承 `BaseEntity`**,因此没有 `deleted` 字段
|
||||||
|
- 保留 `createdAt`、`updatedAt` 用于审计
|
||||||
|
- 主键使用 `@TableId(type = IdType.AUTO)` 自增
|
||||||
|
|
||||||
|
#### 4. Service 层写法
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CourseCollectionPackageService {
|
||||||
|
|
||||||
|
private final CourseCollectionPackageMapper mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新套餐包含的课程包(整包提交模式)
|
||||||
|
* 先删后增,物理删除
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void updatePackages(Long collectionId, List<Long> packageIds) {
|
||||||
|
if (CollectionUtils.isEmpty(packageIds)) {
|
||||||
|
throw new BusinessException("课程包不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 去重(静默处理,不报错)
|
||||||
|
Set<Long> uniquePackageIds = new LinkedHashSet<>(packageIds);
|
||||||
|
|
||||||
|
// 2. 物理删除所有关联(不使用逻辑删除)
|
||||||
|
mapper.delete(new LambdaQueryWrapper<CourseCollectionPackage>()
|
||||||
|
.eq(CourseCollectionPackage::getCollectionId, collectionId));
|
||||||
|
|
||||||
|
// 3. 批量插入
|
||||||
|
List<CourseCollectionPackage> records = new ArrayList<>();
|
||||||
|
int sortOrder = 0;
|
||||||
|
for (Long packageId : uniquePackageIds) {
|
||||||
|
CourseCollectionPackage record = new CourseCollectionPackage();
|
||||||
|
record.setCollectionId(collectionId);
|
||||||
|
record.setPackageId(packageId);
|
||||||
|
record.setSortOrder(sortOrder++);
|
||||||
|
records.add(record);
|
||||||
|
}
|
||||||
|
saveBatch(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个添加课程包(增量添加)
|
||||||
|
*/
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void addPackage(Long collectionId, Long packageId) {
|
||||||
|
// 1. 应用层校验(友好提示)
|
||||||
|
Long count = count(new LambdaQueryWrapper<CourseCollectionPackage>()
|
||||||
|
.eq(CourseCollectionPackage::getCollectionId, collectionId)
|
||||||
|
.eq(CourseCollectionPackage::getPackageId, packageId));
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
throw new BusinessException("该课程包已存在于套餐中");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 插入
|
||||||
|
CourseCollectionPackage record = new CourseCollectionPackage();
|
||||||
|
record.setCollectionId(collectionId);
|
||||||
|
record.setPackageId(packageId);
|
||||||
|
save(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. 数据库表设计
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `course_collection_package` (
|
||||||
|
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
`collection_id` BIGINT NOT NULL COMMENT '课程套餐 ID',
|
||||||
|
`package_id` BIGINT NOT NULL COMMENT '课程包 ID',
|
||||||
|
`sort_order` INT DEFAULT 0 COMMENT '排序号',
|
||||||
|
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
-- 不设置联合唯一索引
|
||||||
|
KEY `idx_collection_id` (`collection_id`),
|
||||||
|
KEY `idx_package_id` (`package_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程套餐 - 课程包关联表';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据一致性保障
|
||||||
|
|
||||||
|
#### 应用层校验
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 1. 保存前去重
|
||||||
|
Set<Long> uniqueIds = new LinkedHashSet<>(ids);
|
||||||
|
|
||||||
|
// 2. 添加前检查
|
||||||
|
Long count = count(new LambdaQueryWrapper<Entity>()
|
||||||
|
.eq(Entity::getField1, value1)
|
||||||
|
.eq(Entity::getField2, value2));
|
||||||
|
if (count > 0) {
|
||||||
|
throw new BusinessException("记录已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 定时任务检查脏数据
|
||||||
|
@Scheduled(cron = "0 0 2 * * ?")
|
||||||
|
public void checkDuplicateData() {
|
||||||
|
// 检查并告警
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 事务保障
|
||||||
|
|
||||||
|
- 所有中间表操作必须使用 `@Transactional` 注解
|
||||||
|
- 先删后增操作在同一事务中完成
|
||||||
|
- 异常时自动回滚
|
||||||
|
|
||||||
|
### B/C/D 类表设计规范
|
||||||
|
|
||||||
|
| 表类型 | deleted | 联合唯一索引 | 说明 |
|
||||||
|
|--------|----------|-------------|------|
|
||||||
|
| B 类(关系表) | ✅ 保留 | ✅ 建议设置 | 如 `parent_student` |
|
||||||
|
| C 类(配置表) | ✅ 保留 | ✅ 必须设置 | 如 `system_setting` |
|
||||||
|
| D 类(合同/授权表) | ✅ 保留 | ❌ 不设置 | 如 `tenant_package` |
|
||||||
|
|
||||||
|
### 决策流程图
|
||||||
|
|
||||||
|
```
|
||||||
|
新表设计 → 是否需要保留删除历史?
|
||||||
|
├─ 否 → 纯关联表(A 类)→ 物理删除,不设置唯一索引
|
||||||
|
└─ 是 → 是否有稳定的业务关系?
|
||||||
|
├─ 是 → 关系表(B 类)→ 逻辑删除,建议设置唯一索引
|
||||||
|
└─ 否 → 配置表/合同表(C/D 类)→ 逻辑删除
|
||||||
|
```
|
||||||
|
|
||||||
|
### 总结
|
||||||
|
|
||||||
|
| 维度 | 传统做法 | 推荐做法(单体应用) |
|
||||||
|
|------|---------|---------------------|
|
||||||
|
| 中间表删除策略 | 逻辑删除 | **物理删除** |
|
||||||
|
| 中间表唯一索引 | 必须设置 | **不设置**(应用层校验) |
|
||||||
|
| 数据一致性 | 数据库兜底 | 应用层校验 + 事务 |
|
||||||
|
| 适用场景 | 高并发、分布式 | 单体应用、低并发 |
|
||||||
|
|||||||
@ -1,18 +1,27 @@
|
|||||||
package com.reading.platform.entity;
|
package com.reading.platform.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 班级教师关系实体
|
* 班级教师关系实体
|
||||||
|
* <p>
|
||||||
|
* 中间表,不设置逻辑删除字段,使用物理删除
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
@Schema(description = "班级教师关系实体")
|
@Schema(description = "班级教师关系实体")
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@TableName("class_teacher")
|
@TableName("class_teacher")
|
||||||
public class ClassTeacher extends BaseEntity {
|
public class ClassTeacher {
|
||||||
|
|
||||||
|
@Schema(description = "主键 ID")
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
@Schema(description = "班级 ID")
|
@Schema(description = "班级 ID")
|
||||||
private Long classId;
|
private Long classId;
|
||||||
@ -23,8 +32,9 @@ public class ClassTeacher extends BaseEntity {
|
|||||||
@Schema(description = "角色")
|
@Schema(description = "角色")
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
// 暂时注释掉数据库中不存在的字段
|
@Schema(description = "创建时间")
|
||||||
// @Schema(description = "是否主班")
|
private LocalDateTime createdAt;
|
||||||
// private Integer isPrimary;
|
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,40 @@
|
|||||||
package com.reading.platform.entity;
|
package com.reading.platform.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 课程套餐与课程包关联实体
|
* 课程套餐与课程包关联实体
|
||||||
|
* <p>
|
||||||
|
* 中间表,不设置逻辑删除字段,使用物理删除
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
@Schema(description = "课程套餐与课程包关联实体")
|
@Schema(description = "课程套餐与课程包关联实体")
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@TableName("course_collection_package")
|
@TableName("course_collection_package")
|
||||||
public class CourseCollectionPackage extends BaseEntity {
|
public class CourseCollectionPackage {
|
||||||
|
|
||||||
@Schema(description = "课程套餐ID")
|
@Schema(description = "主键 ID")
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "课程套餐 ID")
|
||||||
private Long collectionId;
|
private Long collectionId;
|
||||||
|
|
||||||
@Schema(description = "课程包ID")
|
@Schema(description = "课程包 ID")
|
||||||
private Long packageId;
|
private Long packageId;
|
||||||
|
|
||||||
@Schema(description = "排序号")
|
@Schema(description = "排序号")
|
||||||
private Integer sortOrder;
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,27 @@
|
|||||||
package com.reading.platform.entity;
|
package com.reading.platform.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 租户课程关联实体
|
* 租户课程关联实体
|
||||||
|
* <p>
|
||||||
|
* 中间表,不设置逻辑删除字段,使用物理删除
|
||||||
|
* </p>
|
||||||
*/
|
*/
|
||||||
@Schema(description = "租户课程关联实体")
|
@Schema(description = "租户课程关联实体")
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@TableName("tenant_course")
|
@TableName("tenant_course")
|
||||||
public class TenantCourse extends BaseEntity {
|
public class TenantCourse {
|
||||||
|
|
||||||
|
@Schema(description = "主键 ID")
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
@Schema(description = "租户 ID")
|
@Schema(description = "租户 ID")
|
||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
@ -23,4 +32,9 @@ public class TenantCourse extends BaseEntity {
|
|||||||
@Schema(description = "是否启用")
|
@Schema(description = "是否启用")
|
||||||
private Integer enabled;
|
private Integer enabled;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- 迁移 V41: 中间表优化 - 移除联合唯一索引和 deleted 字段
|
||||||
|
-- 版本:V41
|
||||||
|
-- 创建时间:2026-03-19
|
||||||
|
-- 描述:
|
||||||
|
-- 1. 中间表使用物理删除,不再保留 deleted 字段
|
||||||
|
-- 2. 移除不必要的联合唯一索引,通过应用层控制数据一致性
|
||||||
|
-- 3. 适用于单体应用、低并发场景
|
||||||
|
-- =====================================================
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- 1. course_collection_package(课程套餐 - 课程包关联表)
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- 说明:纯关联表,先删后增的更新模式,不需要唯一索引兜底
|
||||||
|
|
||||||
|
-- 删除联合唯一索引
|
||||||
|
ALTER TABLE `course_collection_package` DROP INDEX IF EXISTS `uk_collection_package`;
|
||||||
|
|
||||||
|
-- 删除 deleted 字段
|
||||||
|
ALTER TABLE `course_collection_package` DROP COLUMN IF EXISTS `deleted`;
|
||||||
|
|
||||||
|
-- 保留普通索引(提高查询性能)
|
||||||
|
ALTER TABLE `course_collection_package`
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_collection_id` (`collection_id`),
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_package_id` (`package_id`);
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- 2. tenant_course(租户 - 课程关联表)
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- 说明:纯关联表,授权关系,不需要唯一索引兜底
|
||||||
|
|
||||||
|
-- 删除联合唯一索引
|
||||||
|
ALTER TABLE `tenant_course` DROP INDEX IF EXISTS `uk_tenant_course`;
|
||||||
|
|
||||||
|
-- 删除 deleted 字段
|
||||||
|
ALTER TABLE `tenant_course` DROP COLUMN IF EXISTS `deleted`;
|
||||||
|
|
||||||
|
-- 保留普通索引
|
||||||
|
ALTER TABLE `tenant_course`
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_tenant_id` (`tenant_id`),
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_course_id` (`course_id`);
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- 3. class_teacher(班级 - 教师关联表)
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- 说明:纯关联表,不需要 deleted 字段
|
||||||
|
|
||||||
|
-- 删除 deleted 字段
|
||||||
|
ALTER TABLE `class_teacher` DROP COLUMN IF EXISTS `deleted`;
|
||||||
|
|
||||||
|
-- 保留普通索引
|
||||||
|
ALTER TABLE `class_teacher`
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_class_id` (`class_id`),
|
||||||
|
ADD INDEX IF NOT EXISTS `idx_teacher_id` (`teacher_id`);
|
||||||
|
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- 备注:以下表保留联合唯一索引和 deleted 字段
|
||||||
|
-- -----------------------------------------------------
|
||||||
|
-- 1. parent_student(家长 - 学生关系)
|
||||||
|
-- - 关系相对稳定,有业务历史价值
|
||||||
|
-- - 保留 uk_parent_student 和 deleted 字段
|
||||||
|
--
|
||||||
|
-- 2. system_setting(系统设置)
|
||||||
|
-- - 配置表,(tenant_id, setting_key) 天然应该唯一
|
||||||
|
-- - 保留 uk_tenant_key 和 deleted 字段
|
||||||
|
--
|
||||||
|
-- 3. tenant_package(租户 - 套餐授权)
|
||||||
|
-- - 合同性质,有历史价值
|
||||||
|
-- - 保留 deleted 字段
|
||||||
Loading…
Reference in New Issue
Block a user