refactor(租户管理): 调整配额模态框移除套餐选择器
- 移除调整配额模态框中的套餐选择功能 - quotaForm 数据定义移除 collectionIds 字段 - 简化 handleQuota 函数,仅保留配额相关逻辑 - 使前端与后端 UpdateTenantQuotaDto 接口保持一致 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
de448a3e64
commit
1038a70d92
100
.claude/plans/imperative-honking-kazoo.md
Normal file
100
.claude/plans/imperative-honking-kazoo.md
Normal file
@ -0,0 +1,100 @@
|
||||
# 学校端家长管理"选择孩子"列表字段完善计划
|
||||
|
||||
## 问题背景
|
||||
|
||||
学校端家长管理页面中的"选择孩子"弹窗列表(用于将学生关联到家长)缺少两个关键字段的显示:
|
||||
1. **性别** (gender) - 学生性别
|
||||
2. **所属班级** (className) - 学生所在班级名称
|
||||
|
||||
## 问题分析
|
||||
|
||||
通过代码审查发现:
|
||||
|
||||
### 1. 前端代码 (`ParentListView.vue`)
|
||||
- 第 597-601 行定义了表格列,期望显示 `gender` 和 `className`:
|
||||
```typescript
|
||||
const studentTableColumns = [
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name', width: 120 },
|
||||
{ title: '性别', dataIndex: 'gender', key: 'gender', width: 80 },
|
||||
{ title: '班级', dataIndex: 'className', key: 'className', width: 120 },
|
||||
];
|
||||
```
|
||||
|
||||
### 2. 后端代码 (`SchoolStudentController.java`)
|
||||
- 第 89-119 行的 `getStudentPage` 方法返回学生列表
|
||||
- 目前只设置了 `classId`,未设置 `className`
|
||||
|
||||
### 3. DTO (`StudentResponse.java`)
|
||||
- 有 `gender` 字段(第 29 行)✅
|
||||
- 有 `classId` 字段(第 56 行)✅
|
||||
- **缺少 `className` 字段** ❌
|
||||
|
||||
### 4. 实体类 (`Student.java`)
|
||||
- 有 `gender` 字段(第 26 行)✅
|
||||
|
||||
## 根本原因
|
||||
|
||||
1. `StudentResponse` DTO 缺少 `className` 字段
|
||||
2. Controller 中只查询和设置了 `classId`,没有查询班级名称并设置 `className`
|
||||
|
||||
## 修改方案
|
||||
|
||||
### 1. 添加 `className` 字段到 `StudentResponse`
|
||||
|
||||
**文件**: `reading-platform-java/src/main/java/com/reading/platform/dto/response/StudentResponse.java`
|
||||
|
||||
在 `classId` 字段后添加 `className` 字段:
|
||||
```java
|
||||
@Schema(description = "所在班级 ID")
|
||||
private Long classId;
|
||||
|
||||
@Schema(description = "所在班级名称")
|
||||
private String className;
|
||||
```
|
||||
|
||||
### 2. 在 Controller 中设置 `className`
|
||||
|
||||
**文件**: `reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStudentController.java`
|
||||
|
||||
修改 `getStudentPage` 方法(第 100-118 行):
|
||||
```java
|
||||
for (StudentResponse vo : voList) {
|
||||
// 设置班级
|
||||
var clazz = classService.getPrimaryClassByStudentId(vo.getId());
|
||||
vo.setClassId(clazz != null ? clazz.getId() : null);
|
||||
vo.setClassName(clazz != null ? clazz.getName() : null); // 新增
|
||||
|
||||
// ... 其余代码不变
|
||||
}
|
||||
```
|
||||
|
||||
同样修改 `getStudent` 方法(第 66-86 行),添加 `className` 的设置。
|
||||
|
||||
### 3. 验证 `gender` 字段
|
||||
|
||||
`gender` 字段应该已通过 MapStruct 自动映射。如果前端仍不显示,需要检查:
|
||||
- 数据库中 `gender` 字段是否有值
|
||||
- Mapper 是否正确映射了该字段
|
||||
|
||||
## 关键文件列表
|
||||
|
||||
| 文件路径 | 修改类型 | 说明 |
|
||||
|---------|---------|------|
|
||||
| `reading-platform-java/src/main/java/com/reading/platform/dto/response/StudentResponse.java` | 修改 | 添加 `className` 字段 |
|
||||
| `reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolStudentController.java` | 修改 | 设置 `className` 字段值 |
|
||||
|
||||
## 验证步骤
|
||||
|
||||
1. 启动后端服务
|
||||
2. 进入学校端家长管理页面
|
||||
3. 点击任意家长的"孩子"按钮打开管理弹窗
|
||||
4. 点击"添加孩子"打开选择学生弹窗
|
||||
5. 验证表格中是否正确显示:
|
||||
- 性别列(男/女)
|
||||
- 班级列(班级名称)
|
||||
|
||||
## 风险评估
|
||||
|
||||
- **低风险**:只是添加一个返回字段,不影响现有功能
|
||||
- **向后兼容**:前端已准备好接收这些字段,不会破坏现有功能
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
VITE_API_BASE_URL=
|
||||
VITE_APP_TITLE=幼儿阅读教学服务平台
|
||||
VITE_SERVER_BASE_URL=
|
||||
VITE_ENABLE_DEFAULT_ACCOUNT=true
|
||||
|
||||
4
reading-platform-frontend/.env.production
Normal file
4
reading-platform-frontend/.env.production
Normal file
@ -0,0 +1,4 @@
|
||||
VITE_API_BASE_URL=
|
||||
VITE_APP_TITLE=幼儿阅读教学服务平台
|
||||
VITE_SERVER_BASE_URL=
|
||||
VITE_ENABLE_DEFAULT_ACCOUNT=false
|
||||
@ -1,2 +1,3 @@
|
||||
VITE_APP_PORT=5174
|
||||
VITE_BACKEND_PORT=8481
|
||||
VITE_ENABLE_DEFAULT_ACCOUNT=true
|
||||
|
||||
@ -56,7 +56,19 @@
|
||||
<video v-if="fileType === 'video'" :src="previewUrl" controls style="width: 100%" />
|
||||
<audio v-else-if="fileType === 'audio'" :src="previewUrl" controls style="width: 100%" />
|
||||
<iframe v-else-if="fileType === 'pdf' || isPdfFile" :src="previewUrl" style="width: 100%; height: 600px; border: none" />
|
||||
<img v-else-if="fileType === 'image'" :src="previewUrl" style="max-width: 100%" />
|
||||
<img
|
||||
v-else-if="fileType === 'image'"
|
||||
:src="previewUrl"
|
||||
style="max-width: 100%"
|
||||
@load="imagePreviewLoading = false; imagePreviewLoaded = true"
|
||||
@error="imagePreviewLoading = false; imagePreviewError = true"
|
||||
:style="{ opacity: imagePreviewLoaded ? 1 : 0, transition: 'opacity 0.3s' }"
|
||||
/>
|
||||
<!-- 图片加载失败提示 -->
|
||||
<div v-if="fileType === 'image' && imagePreviewError" class="image-error-tip">
|
||||
<FileOutlined style="font-size: 48px; color: #999" />
|
||||
<p>图片加载失败</p>
|
||||
</div>
|
||||
<div v-else class="unsupported">
|
||||
<FileOutlined style="font-size: 48px; color: #999" />
|
||||
<p>暂不支持预览此类型文件</p>
|
||||
@ -103,6 +115,20 @@ const emit = defineEmits<{
|
||||
const previewVisible = ref(false);
|
||||
const uploading = ref(false);
|
||||
|
||||
// 图片预览加载状态
|
||||
const imagePreviewLoading = ref(false);
|
||||
const imagePreviewLoaded = ref(false);
|
||||
const imagePreviewError = ref(false);
|
||||
|
||||
// 监听 previewUrl 变化,重置加载状态
|
||||
watch(() => previewUrl.value, (newUrl) => {
|
||||
if (newUrl && props.fileType === 'image') {
|
||||
imagePreviewLoading.value = true;
|
||||
imagePreviewLoaded.value = false;
|
||||
imagePreviewError.value = false;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const buttonText = computed(() => {
|
||||
const typeMap: Record<string, string> = {
|
||||
video: '上传视频',
|
||||
@ -300,6 +326,19 @@ const handleRemove = () => {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.image-error-tip {
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
p {
|
||||
margin: 16px 0;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -174,21 +174,6 @@
|
||||
<a-form-item label="当前租户">
|
||||
<span>{{ currentTenant?.name }}</span>
|
||||
</a-form-item>
|
||||
<a-form-item label="套餐">
|
||||
<a-select
|
||||
v-model:value="quotaForm.collectionIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择套餐"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="pkg in packageList"
|
||||
:key="pkg.id"
|
||||
:value="pkg.id"
|
||||
>
|
||||
{{ pkg.name }} ({{ formatPackagePrice(pkg.discountPrice || pkg.price) }}元{{ pkg.discountType ? ' ' + getDiscountTypeText(pkg.discountType) : '' }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="教师配额">
|
||||
<a-input-number v-model:value="quotaForm.teacherQuota" :min="currentTenant?.teacherCount || 1" :max="1000"
|
||||
style="width: 100%" />
|
||||
@ -455,11 +440,9 @@ const quotaModalVisible = ref(false);
|
||||
const quotaModalLoading = ref(false);
|
||||
const currentTenant = ref<Tenant | null>(null);
|
||||
const quotaForm = reactive<{
|
||||
collectionIds?: number[];
|
||||
teacherQuota: number;
|
||||
studentQuota: number;
|
||||
}>({
|
||||
collectionIds: undefined,
|
||||
teacherQuota: 20,
|
||||
studentQuota: 200,
|
||||
});
|
||||
@ -593,8 +576,8 @@ const handleModalOk = async () => {
|
||||
}
|
||||
|
||||
modalLoading.value = true;
|
||||
try {
|
||||
const [startDate, expireDate] = formData.dateRange || [];
|
||||
try {
|
||||
const data = {
|
||||
...formData,
|
||||
startDate,
|
||||
@ -685,15 +668,6 @@ const handleViewDetail = async (record: Tenant) => {
|
||||
// 配额调整
|
||||
const handleQuota = (record: Tenant) => {
|
||||
currentTenant.value = record;
|
||||
// 根据套餐名称获取对应的套餐 ID 列表
|
||||
if (record.packageNames && record.packageNames.length > 0) {
|
||||
quotaForm.collectionIds = record.packageNames.map(name => {
|
||||
const pkg = packageList.value.find(p => p.name === name);
|
||||
return pkg?.id;
|
||||
}).filter(id => id !== undefined) as number[];
|
||||
} else {
|
||||
quotaForm.collectionIds = undefined;
|
||||
}
|
||||
quotaForm.teacherQuota = record.teacherQuota;
|
||||
quotaForm.studentQuota = record.studentQuota;
|
||||
quotaModalVisible.value = true;
|
||||
|
||||
@ -110,6 +110,9 @@ import { useUserStore } from '@/stores/user';
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 是否启用默认账号填充(开发/测试环境启用,生产环境禁用)
|
||||
const enableDefaultAccount = import.meta.env.VITE_ENABLE_DEFAULT_ACCOUNT === 'true';
|
||||
|
||||
const roles = [
|
||||
{ value: 'admin', label: '超管', icon: markRaw(SettingOutlined) },
|
||||
{ value: 'school', label: '学校', icon: markRaw(SolutionOutlined) },
|
||||
@ -126,8 +129,8 @@ const testAccounts = [
|
||||
|
||||
const formState = reactive({
|
||||
role: 'admin',
|
||||
account: 'admin',
|
||||
password: '123456',
|
||||
account: enableDefaultAccount ? 'admin' : '',
|
||||
password: enableDefaultAccount ? '123456' : '',
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
@ -143,6 +146,7 @@ const fillAccount = (acc: any) => {
|
||||
};
|
||||
|
||||
watch(() => formState.role, (role) => {
|
||||
if (!enableDefaultAccount) return; // 生产环境不自动填充
|
||||
const found = testAccounts.find(t => t.role === role);
|
||||
if (found) {
|
||||
formState.account = found.account;
|
||||
|
||||
@ -122,7 +122,6 @@ import {
|
||||
type CoursePackage,
|
||||
type FilterMetaResponse,
|
||||
} from '@/api/course-center';
|
||||
import { getCoursePackageDetail } from '@/api/school';
|
||||
import CoursePackageCard from './components/CoursePackageCard.vue';
|
||||
|
||||
const router = useRouter();
|
||||
@ -249,11 +248,20 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
// 学校端主题配色 - 橙色主题
|
||||
$primary-color: #FF8C42; // 温暖的橙色
|
||||
$primary-light: #FFF4EC;
|
||||
$primary-dark: #E67635;
|
||||
$bg-color: #F5F7FA;
|
||||
$text-color: #333;
|
||||
$text-secondary: #666;
|
||||
$border-color: #F0F0F0;
|
||||
|
||||
.course-center-page {
|
||||
display: flex;
|
||||
min-height: calc(100vh - 120px);
|
||||
background: #F5F7FA;
|
||||
background: $bg-color;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
@ -270,14 +278,14 @@ onMounted(() => {
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #F0F0F0;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.collection-list {
|
||||
@ -296,18 +304,18 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.collection-item:hover {
|
||||
background: #F5F7FA;
|
||||
background: $bg-color;
|
||||
}
|
||||
|
||||
.collection-item.active {
|
||||
background: #E6F7FF;
|
||||
border-left-color: #1890ff;
|
||||
background: $primary-light;
|
||||
border-left-color: $primary-color;
|
||||
}
|
||||
|
||||
.collection-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
color: $text-color;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@ -340,7 +348,7 @@ onMounted(() => {
|
||||
.collection-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: $text-color;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
@ -371,7 +379,7 @@ onMounted(() => {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #1890ff;
|
||||
color: $primary-color;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -434,12 +442,12 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.grade-tag:hover {
|
||||
background: #E6F7FF;
|
||||
color: #1890ff;
|
||||
background: $primary-light;
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
.grade-tag.active {
|
||||
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
|
||||
background: linear-gradient(135deg, $primary-color 0%, $primary-dark 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@ -513,7 +521,7 @@ onMounted(() => {
|
||||
.lessons-section h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: $text-color;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@ -529,7 +537,7 @@ onMounted(() => {
|
||||
|
||||
.lesson-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
|
||||
@ -2,12 +2,23 @@
|
||||
<div class="package-card" @click="handleClick">
|
||||
<!-- 封面区域 -->
|
||||
<div class="cover-wrapper">
|
||||
<div v-if="pkg.coverImagePath" class="cover-image-wrapper">
|
||||
<a-spin :spinning="coverLoading" :indicator="null">
|
||||
<img
|
||||
v-if="pkg.coverImagePath"
|
||||
:src="getImageUrl(pkg.coverImagePath)"
|
||||
class="cover-image"
|
||||
alt="课程包封面"
|
||||
:style="{ opacity: coverLoaded ? 1 : 0, transition: 'opacity 0.3s' }"
|
||||
@load="coverLoading = false; coverLoaded = true"
|
||||
@error="coverLoading = false; coverError = true"
|
||||
/>
|
||||
</a-spin>
|
||||
<!-- 加载失败占位图 -->
|
||||
<div v-if="coverError" class="error-placeholder">
|
||||
<BookFilled class="placeholder-icon" />
|
||||
<span class="placeholder-text">封面加载失败</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="cover-placeholder">
|
||||
<BookFilled class="placeholder-icon" />
|
||||
<span class="placeholder-text">精彩绘本</span>
|
||||
@ -54,7 +65,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import {
|
||||
BookOutlined,
|
||||
BookFilled,
|
||||
@ -69,6 +80,18 @@ const props = defineProps<{
|
||||
pkg: CoursePackage;
|
||||
}>();
|
||||
|
||||
// 封面图加载状态
|
||||
const coverLoading = ref(true);
|
||||
const coverLoaded = ref(false);
|
||||
const coverError = ref(false);
|
||||
|
||||
// 监听 coverImagePath 变化,重置加载状态
|
||||
watch(() => props.pkg.coverImagePath, () => {
|
||||
coverLoading.value = true;
|
||||
coverLoaded.value = false;
|
||||
coverError.value = false;
|
||||
}, { immediate: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', pkg: CoursePackage): void;
|
||||
(e: 'view', pkg: CoursePackage): void;
|
||||
@ -112,7 +135,12 @@ const handleView = () => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="scss">
|
||||
// 学校端主题配色 - 橙色主题
|
||||
$primary-color: #FF8C42;
|
||||
$primary-light: #FFF4EC;
|
||||
$primary-dark: #E67635;
|
||||
|
||||
.package-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
@ -125,8 +153,8 @@ const handleView = () => {
|
||||
|
||||
.package-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 20px rgba(24, 144, 255, 0.15);
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 8px 20px rgba(255, 140, 66, 0.15);
|
||||
border-color: $primary-color;
|
||||
}
|
||||
|
||||
/* 封面区域 */
|
||||
@ -136,11 +164,42 @@ const handleView = () => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
.cover-image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.error-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, $primary-light 0%, #f0f5ff 100%);
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 48px;
|
||||
color: $primary-color;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 14px;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.package-card:hover .cover-image {
|
||||
@ -154,18 +213,18 @@ const handleView = () => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%);
|
||||
background: linear-gradient(135deg, $primary-light 0%, #f0f5ff 100%);
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 48px;
|
||||
color: #1890ff;
|
||||
color: $primary-color;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
|
||||
@ -2,12 +2,23 @@
|
||||
<div class="package-card" @click="handleClick">
|
||||
<!-- 封面区域 -->
|
||||
<div class="cover-wrapper">
|
||||
<div v-if="pkg.coverImagePath" class="cover-image-wrapper">
|
||||
<a-spin :spinning="coverLoading" :indicator="null">
|
||||
<img
|
||||
v-if="pkg.coverImagePath"
|
||||
:src="getImageUrl(pkg.coverImagePath)"
|
||||
class="cover-image"
|
||||
alt="课程包封面"
|
||||
:style="{ opacity: coverLoaded ? 1 : 0, transition: 'opacity 0.3s' }"
|
||||
@load="coverLoading = false; coverLoaded = true"
|
||||
@error="coverLoading = false; coverError = true"
|
||||
/>
|
||||
</a-spin>
|
||||
<!-- 加载失败占位图 -->
|
||||
<div v-if="coverError" class="error-placeholder">
|
||||
<BookFilled class="placeholder-icon" />
|
||||
<span class="placeholder-text">封面加载失败</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="cover-placeholder">
|
||||
<BookFilled class="placeholder-icon" />
|
||||
<span class="placeholder-text">精彩绘本</span>
|
||||
@ -53,7 +64,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import {
|
||||
BookOutlined,
|
||||
BookFilled,
|
||||
@ -68,6 +79,18 @@ const props = defineProps<{
|
||||
pkg: CoursePackage;
|
||||
}>();
|
||||
|
||||
// 封面图加载状态
|
||||
const coverLoading = ref(true);
|
||||
const coverLoaded = ref(false);
|
||||
const coverError = ref(false);
|
||||
|
||||
// 监听 coverImagePath 变化,重置加载状态
|
||||
watch(() => props.pkg.coverImagePath, () => {
|
||||
coverLoading.value = true;
|
||||
coverLoaded.value = false;
|
||||
coverError.value = false;
|
||||
}, { immediate: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click', pkg: CoursePackage): void;
|
||||
(e: 'prepare', pkg: CoursePackage): void;
|
||||
@ -134,11 +157,42 @@ const handlePrepare = () => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
.cover-image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.error-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #FFE4C9 0%, #FFF0E0 100%);
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 48px;
|
||||
color: #FF8C42;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 14px;
|
||||
color: #FF8C42;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.package-card:hover .cover-image {
|
||||
|
||||
@ -14,7 +14,22 @@
|
||||
class="resource-item image-item"
|
||||
@click="handlePreview('image', image)"
|
||||
>
|
||||
<img :src="getFileUrl(image.path)" :alt="image.name || `图片${idx + 1}`" />
|
||||
<div class="image-wrapper">
|
||||
<a-spin :spinning="imageLoadingStates[idx] === 'loading'" :indicator="null">
|
||||
<img
|
||||
:src="getFileUrl(image.path)"
|
||||
:alt="image.name || `图片${idx + 1}`"
|
||||
:style="{ display: imageLoadingStates[idx] === 'loaded' ? 'block' : 'none' }"
|
||||
@load="imageLoadingStates[idx] = 'loaded'"
|
||||
@error="imageLoadingStates[idx] = 'error'"
|
||||
/>
|
||||
</a-spin>
|
||||
<!-- 加载失败占位图 -->
|
||||
<div v-if="imageLoadingStates[idx] === 'error'" class="error-placeholder">
|
||||
<PictureOutlined style="font-size: 32px; color: #999;" />
|
||||
<span>加载失败</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource-info">
|
||||
<div class="resource-name">{{ image.name || `图片${idx + 1}` }}</div>
|
||||
<div class="resource-action">点击预览</div>
|
||||
@ -113,7 +128,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
PictureOutlined, VideoCameraOutlined, AudioOutlined,
|
||||
@ -128,6 +143,24 @@ const emit = defineEmits<{
|
||||
'preview-resource': [type: string, resource: any];
|
||||
}>();
|
||||
|
||||
// 图片加载状态管理:'loading' | 'loaded' | 'error'
|
||||
const imageLoadingStates = ref<Record<number, 'loading' | 'loaded' | 'error'>>({});
|
||||
|
||||
// 监听 lesson.images 变化,重置加载状态
|
||||
watch(() => props.lesson?.images, (newImages) => {
|
||||
if (!newImages) {
|
||||
imageLoadingStates.value = {};
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置所有图片的加载状态为 'loading'
|
||||
const newStates: Record<number, 'loading' | 'loaded' | 'error'> = {};
|
||||
newImages.forEach((_: any, idx: number) => {
|
||||
newStates[idx] = 'loading';
|
||||
});
|
||||
imageLoadingStates.value = newStates;
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
const hasResources = computed(() => {
|
||||
const l = props.lesson;
|
||||
return (l.images?.length || 0) +
|
||||
@ -220,14 +253,48 @@ const handlePreview = (type: string, resource: any) => {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.image-item img {
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.image-wrapper img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.image-wrapper :deep(.ant-spin) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.resource-icon {
|
||||
font-size: 32px;
|
||||
color: #FF8C42;
|
||||
|
||||
@ -43,8 +43,22 @@
|
||||
<div class="record-grid" v-if="!loading && records.length > 0">
|
||||
<div v-for="record in records" :key="record.id" class="record-card">
|
||||
<div class="card-cover">
|
||||
<div v-if="record.images?.length" class="cover-image ">
|
||||
<img :src="getImageUrl(record.images[0])" class="pos-absolute !object-contain " />
|
||||
<div v-if="record.images?.length" class="cover-image">
|
||||
<div class="image-wrapper">
|
||||
<a-spin :spinning="recordLoadingStates[record.id]" :indicator="null">
|
||||
<img
|
||||
:src="getImageUrl(record.images[0])"
|
||||
class="pos-absolute !object-contain"
|
||||
:style="{ opacity: recordImageLoaded[record.id] ? 1 : 0, transition: 'opacity 0.3s' }"
|
||||
@load="handleImageLoad(record.id)"
|
||||
@error="handleImageError(record.id)"
|
||||
/>
|
||||
</a-spin>
|
||||
<!-- 加载失败占位图 -->
|
||||
<div v-if="recordImageError[record.id]" class="error-placeholder">
|
||||
<FileTextOutlined class="placeholder-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-count" v-if="record.images.length > 1">
|
||||
<CameraOutlined /> {{ record.images.length }}
|
||||
</div>
|
||||
@ -231,6 +245,33 @@ import {
|
||||
type CreateGrowthRecordDto,
|
||||
type UpdateGrowthRecordDto,
|
||||
} from '@/api/growth';
|
||||
|
||||
// 图片加载状态管理
|
||||
const recordLoadingStates = ref<Record<number, boolean>>({});
|
||||
const recordImageLoaded = ref<Record<number, boolean>>({});
|
||||
const recordImageError = ref<Record<number, boolean>>({});
|
||||
|
||||
// 监听 records 变化,初始化加载状态
|
||||
const handleFilter = () => {
|
||||
recordLoadingStates.value = {};
|
||||
recordImageLoaded.value = {};
|
||||
recordImageError.value = {};
|
||||
pagination.current = 1;
|
||||
loadRecords();
|
||||
};
|
||||
|
||||
const handleImageLoad = (recordId: number) => {
|
||||
recordLoadingStates.value[recordId] = false;
|
||||
recordImageLoaded.value[recordId] = true;
|
||||
recordImageError.value[recordId] = false;
|
||||
};
|
||||
|
||||
const handleImageError = (recordId: number) => {
|
||||
recordLoadingStates.value[recordId] = false;
|
||||
recordImageLoaded.value[recordId] = false;
|
||||
recordImageError.value[recordId] = true;
|
||||
};
|
||||
|
||||
import { getTeacherClasses, getTeacherStudents } from '@/api/teacher';
|
||||
import { fileApi, validateFileType } from '@/api/file';
|
||||
|
||||
@ -324,11 +365,6 @@ const loadStudents = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilter = () => {
|
||||
pagination.current = 1;
|
||||
loadRecords();
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
pagination.current = page;
|
||||
pagination.pageSize = pageSize;
|
||||
@ -606,6 +642,36 @@ onMounted(() => {
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 48px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cover-image img {
|
||||
|
||||
@ -24,7 +24,23 @@
|
||||
<!-- 图片展示 -->
|
||||
<div v-else-if="currentResourceType === 'image'" key="image" class="content-viewer">
|
||||
<div class="image-viewer">
|
||||
<img :src="currentResourceUrl" :alt="currentResourceName" class="full-image" />
|
||||
<div class="image-wrapper">
|
||||
<a-spin :spinning="imageLoading" :indicator="null">
|
||||
<img
|
||||
:src="currentResourceUrl"
|
||||
:alt="currentResourceName"
|
||||
class="full-image"
|
||||
:style="{ opacity: imageLoaded ? 1 : 0, transition: 'opacity 0.3s' }"
|
||||
@load="onImageLoad"
|
||||
@error="onImageError"
|
||||
/>
|
||||
</a-spin>
|
||||
<!-- 加载失败占位图 -->
|
||||
<div v-if="imageError" class="error-placeholder">
|
||||
<Image style="font-size: 48px; color: #999;" />
|
||||
<span>图片加载失败</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentResourceName" class="image-caption">{{ currentResourceName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -147,8 +163,22 @@
|
||||
<div v-if="activityResourceUrl" class="activity-media">
|
||||
<video v-if="activityResourceType === 'video'" :src="activityResourceUrl" controls
|
||||
style="width: 100%; max-height: 50vh;"></video>
|
||||
<img v-else-if="activityResourceType === 'image'" :src="activityResourceUrl"
|
||||
style="width: 100%; max-height: 50vh; object-fit: contain;">
|
||||
<div v-else-if="activityResourceType === 'image'" class="activity-image-wrapper">
|
||||
<a-spin :spinning="activityImageLoading" :indicator="null">
|
||||
<img
|
||||
:src="activityResourceUrl"
|
||||
style="width: 100%; max-height: 50vh; object-fit: contain; display: block;"
|
||||
:style="{ opacity: activityImageLoaded ? 1 : 0, transition: 'opacity 0.3s' }"
|
||||
@load="activityImageLoaded = true"
|
||||
@error="activityImageError = true"
|
||||
/>
|
||||
</a-spin>
|
||||
<!-- 加载失败占位图 -->
|
||||
<div v-if="activityImageError" class="error-placeholder">
|
||||
<Image style="font-size: 48px; color: #999;" />
|
||||
<span>图片加载失败</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-info">
|
||||
<div class="info-item" v-if="selectedActivity.objectives">
|
||||
@ -173,7 +203,6 @@
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||
import {
|
||||
BookOpen,
|
||||
Check,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
X,
|
||||
@ -232,6 +261,16 @@ const currentEbookPage = ref(0);
|
||||
const currentSlidePage = ref(0);
|
||||
const currentDocumentPage = ref(0);
|
||||
|
||||
// 图片加载状态
|
||||
const imageLoading = ref(false);
|
||||
const imageLoaded = ref(false);
|
||||
const imageError = ref(false);
|
||||
|
||||
// 活动弹窗中的图片加载状态
|
||||
const activityImageLoading = ref(false);
|
||||
const activityImageLoaded = ref(false);
|
||||
const activityImageError = ref(false);
|
||||
|
||||
// 绘本音频同步
|
||||
const syncAudioUrl = ref<string>('');
|
||||
const autoPlayAudio = ref(false);
|
||||
@ -788,6 +827,19 @@ const handleExit = () => {
|
||||
emit('exit');
|
||||
};
|
||||
|
||||
// 图片加载事件处理
|
||||
const onImageLoad = () => {
|
||||
imageLoading.value = false;
|
||||
imageLoaded.value = true;
|
||||
imageError.value = false;
|
||||
};
|
||||
|
||||
const onImageError = () => {
|
||||
imageLoading.value = false;
|
||||
imageLoaded.value = false;
|
||||
imageError.value = true;
|
||||
};
|
||||
|
||||
// 根据索引加载并显示资源
|
||||
const handleResourceByIndex = (index: number) => {
|
||||
playSound('click');
|
||||
@ -797,6 +849,13 @@ const handleResourceByIndex = (index: number) => {
|
||||
currentResourceType.value = resource.type;
|
||||
currentResourceUrl.value = resource.url;
|
||||
currentResourceName.value = resource.name;
|
||||
|
||||
// 如果是图片类型,重置加载状态
|
||||
if (resource.type === 'image') {
|
||||
imageLoading.value = true;
|
||||
imageLoaded.value = false;
|
||||
imageError.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -866,12 +925,23 @@ const handleDocumentPageChange = (page: number) => {
|
||||
const handleActivityClick = (activity: any) => {
|
||||
selectedActivity.value = activity;
|
||||
playSound('click');
|
||||
|
||||
// 重置活动图片加载状态
|
||||
activityImageLoading.value = false;
|
||||
activityImageLoaded.value = false;
|
||||
activityImageError.value = false;
|
||||
|
||||
if (activity.onlineMaterials) {
|
||||
try {
|
||||
const materials = typeof activity.onlineMaterials === 'string' ? JSON.parse(activity.onlineMaterials) : activity.onlineMaterials;
|
||||
if (materials.length > 0) {
|
||||
activityResourceUrl.value = getFileUrl(materials[0].path);
|
||||
activityResourceType.value = materials[0].type || 'image';
|
||||
|
||||
// 如果是图片类型,设置 loading 状态
|
||||
if (materials[0].type === 'image') {
|
||||
activityImageLoading.value = true;
|
||||
}
|
||||
}
|
||||
} catch { activityResourceUrl.value = ''; }
|
||||
}
|
||||
@ -1192,6 +1262,14 @@ onUnmounted(() => {
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
|
||||
.image-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
max-height: calc(100% - 60px);
|
||||
|
||||
.full-image {
|
||||
max-width: 100%;
|
||||
max-height: calc(100% - 60px);
|
||||
@ -1200,6 +1278,24 @@ onUnmounted(() => {
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.error-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.image-caption {
|
||||
margin-top: 16px;
|
||||
font-size: 18px;
|
||||
@ -1774,6 +1870,32 @@ onUnmounted(() => {
|
||||
background: white;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
border: 3px solid #FFE0B2;
|
||||
|
||||
.activity-image-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-height: 50vh;
|
||||
|
||||
.error-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
:src="pages[currentPage]"
|
||||
:key="currentPage"
|
||||
class="slide-image"
|
||||
:style="{ opacity: isLoading ? 0 : 1, transition: 'opacity 0.3s' }"
|
||||
@load="onImageLoad"
|
||||
@error="onImageError"
|
||||
alt="幻灯片"
|
||||
@ -109,7 +110,6 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Presentation,
|
||||
FileText,
|
||||
Download,
|
||||
ExternalLink,
|
||||
} from 'lucide-vue-next';
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.reading.platform.common.aspect;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.filter.ValueFilter;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.reading.platform.common.annotation.Log;
|
||||
import com.reading.platform.common.security.SecurityUtils;
|
||||
@ -14,7 +15,6 @@ import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.AfterThrowing;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
@ -43,6 +43,24 @@ public class LogAspect {
|
||||
*/
|
||||
private static final ThreadLocal<OperationLog> LOG_LOCAL = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* FastJSON2 值过滤器 - 用于脱敏敏感字段
|
||||
*/
|
||||
private static final ValueFilter SENSITIVE_FILTER = (obj, name, value) -> {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
// 检查字段名是否包含敏感词
|
||||
String lowerName = name.toLowerCase();
|
||||
if (lowerName.contains("password") ||
|
||||
lowerName.contains("passwd") ||
|
||||
lowerName.contains("secret") ||
|
||||
lowerName.contains("token")) {
|
||||
return "***";
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* 前置通知:在方法执行前记录日志
|
||||
*
|
||||
@ -98,9 +116,8 @@ public class LogAspect {
|
||||
if (logAnnotation.recordParams()) {
|
||||
try {
|
||||
Object[] params = getRequestParams(joinPoint);
|
||||
// 对参数进行脱敏处理(移除敏感信息)
|
||||
Object[] sanitizedParams = desensitizeParams(joinPoint, params);
|
||||
String paramsJson = JSON.toJSONString(sanitizedParams);
|
||||
// 使用 ValueFilter 脱敏敏感字段后序列化
|
||||
String paramsJson = JSON.toJSONString(params, SENSITIVE_FILTER);
|
||||
operationLog.setRequestParams(paramsJson);
|
||||
} catch (Exception e) {
|
||||
log.warn("记录请求参数失败:{}", e.getMessage());
|
||||
@ -209,68 +226,4 @@ public class LogAspect {
|
||||
private Object[] getRequestParams(JoinPoint joinPoint) {
|
||||
return joinPoint.getArgs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对请求参数进行脱敏处理(密码字段)
|
||||
* <p>
|
||||
* 注意:DTO 类中的密码字段使用 @JsonIgnore 注解,序列化时会自动忽略
|
||||
* 本方法主要处理 String 类型的直接参数(如 oldPassword, newPassword)
|
||||
* </p>
|
||||
*
|
||||
* @param joinPoint 切面连接点
|
||||
* @param params 原始参数
|
||||
* @return 脱敏后的参数
|
||||
*/
|
||||
private Object[] desensitizeParams(JoinPoint joinPoint, Object[] params) {
|
||||
if (params == null || params.length == 0) {
|
||||
return params;
|
||||
}
|
||||
|
||||
Object[] sanitized = new Object[params.length];
|
||||
for (int i = 0; i < params.length; i++) {
|
||||
if (params[i] instanceof String strParam) {
|
||||
// 检查参数名是否包含敏感词(通过方法参数名判断)
|
||||
String paramName = getParameterName(joinPoint, i);
|
||||
if (isSensitiveParam(paramName)) {
|
||||
sanitized[i] = "***";
|
||||
} else {
|
||||
sanitized[i] = strParam;
|
||||
}
|
||||
} else {
|
||||
// DTO 对象:依赖 @JsonIgnore 注解自动脱敏
|
||||
sanitized[i] = params[i];
|
||||
}
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数名
|
||||
*/
|
||||
private String getParameterName(JoinPoint joinPoint, int paramIndex) {
|
||||
try {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
String[] paramNames = signature.getParameterNames();
|
||||
if (paramNames != null && paramIndex < paramNames.length) {
|
||||
return paramNames[paramIndex];
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("获取参数名失败:{}", e.getMessage());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为敏感参数名
|
||||
*/
|
||||
private boolean isSensitiveParam(String paramName) {
|
||||
if (paramName == null || paramName.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String lowerName = paramName.toLowerCase();
|
||||
return lowerName.contains("password") ||
|
||||
lowerName.contains("passwd") ||
|
||||
lowerName.contains("secret") ||
|
||||
lowerName.contains("token");
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ public enum ErrorCode {
|
||||
|
||||
// Package Errors (3100+)
|
||||
PACKAGE_NOT_FOUND(3101, "套餐不存在"),
|
||||
REMOVE_PACKAGE_HAS_SCHEDULES(3102, "该套餐下有排课计划,请确认是否强制移除"),
|
||||
REMOVE_PACKAGE_HAS_SCHEDULES(3102, "该套餐下有排课计划,无法删除"),
|
||||
|
||||
// User Errors (4000+)
|
||||
USER_NOT_FOUND(4001, "用户不存在"),
|
||||
|
||||
@ -240,7 +240,7 @@ public class TenantServiceImpl extends com.baomidou.mybatisplus.extension.servic
|
||||
warningList.add((Map<String, Object>) warning);
|
||||
}
|
||||
throw new BusinessException(ErrorCode.REMOVE_PACKAGE_HAS_SCHEDULES,
|
||||
"该套餐下有排课计划,请确认是否强制移除", warningList);
|
||||
"该套餐下有排课计划,无法删除", warningList);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user