fix: 套餐管理优化与 gradeLevels/重复包修复
- gradeLevels: 后端 JSON 解析修复,前端兼容错误格式 - 筛选下拉: 状态选项按业务流程排序 - 已下架: 列表与详情页增加编辑、删除操作 - 课程包关联: 前后端去重,修复 uk_collection_package 唯一约束冲突 Made-with: Cursor
This commit is contained in:
parent
289fcbee52
commit
8f5a0fcdda
@ -202,9 +202,21 @@ export function getCollectionStatusInfo(status: string) {
|
|||||||
return COLLECTION_STATUS_MAP[status] || { label: status, color: 'default' };
|
return COLLECTION_STATUS_MAP[status] || { label: status, color: 'default' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析适用年级
|
// 解析适用年级(统一处理列表展示与创建时的格式)
|
||||||
export function parseGradeLevels(gradeLevels: string | string[]): string[] {
|
export function parseGradeLevels(gradeLevels: string | string[] | undefined): string[] {
|
||||||
if (Array.isArray(gradeLevels)) return gradeLevels;
|
if (!gradeLevels) return [];
|
||||||
|
if (Array.isArray(gradeLevels)) {
|
||||||
|
if (gradeLevels.length === 0) return [];
|
||||||
|
// 兼容后端错误格式:["[\"小班\"", " \"中班\""] -> 拼接后解析
|
||||||
|
if (gradeLevels[0]?.toString().startsWith('[')) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(gradeLevels.join(''));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gradeLevels;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(gradeLevels || '[]');
|
return JSON.parse(gradeLevels || '[]');
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -31,10 +31,14 @@
|
|||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 已下架状态:显示重新发布按钮 -->
|
<!-- 已下架状态:显示编辑、删除、重新发布按钮 -->
|
||||||
<template v-else-if="collection?.status === 'ARCHIVED'">
|
<template v-else-if="collection?.status === 'ARCHIVED'">
|
||||||
|
<a-button type="primary" @click="editCollection">编辑</a-button>
|
||||||
<a-popconfirm title="确定要重新发布此套餐吗?" @confirm="handleRepublish">
|
<a-popconfirm title="确定要重新发布此套餐吗?" @confirm="handleRepublish">
|
||||||
<a-button type="primary">重新发布</a-button>
|
<a-button>重新发布</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
<a-popconfirm title="确定要删除此套餐吗?" @confirm="handleDelete">
|
||||||
|
<a-button danger>删除</a-button>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -284,10 +288,12 @@ const handleAddPackage = async () => {
|
|||||||
|
|
||||||
addingPackage.value = true;
|
addingPackage.value = true;
|
||||||
try {
|
try {
|
||||||
// 获取当前课程包列表
|
// 获取当前课程包列表(去重)
|
||||||
const currentPackageIds = collection.value?.packages?.map(p => p.id) || [];
|
const currentPackageIds = [...new Set(collection.value?.packages?.map(p => p.id) || [])];
|
||||||
// 添加新的课程包ID
|
// 添加新的课程包ID(避免重复)
|
||||||
const newPackageIds = [...currentPackageIds, selectedPackageId.value];
|
const newPackageIds = currentPackageIds.includes(selectedPackageId.value)
|
||||||
|
? currentPackageIds
|
||||||
|
: [...currentPackageIds, selectedPackageId.value];
|
||||||
|
|
||||||
await collectionsApi.setCollectionPackages(route.params.id, newPackageIds);
|
await collectionsApi.setCollectionPackages(route.params.id, newPackageIds);
|
||||||
message.success('添加成功');
|
message.success('添加成功');
|
||||||
@ -304,8 +310,8 @@ const handleAddPackage = async () => {
|
|||||||
// 移除课程包
|
// 移除课程包
|
||||||
const handleRemovePackage = async (packageId: number) => {
|
const handleRemovePackage = async (packageId: number) => {
|
||||||
try {
|
try {
|
||||||
// 获取当前课程包列表,移除指定的课程包
|
// 获取当前课程包列表(去重),移除指定的课程包
|
||||||
const currentPackageIds = collection.value?.packages?.map(p => p.id) || [];
|
const currentPackageIds = [...new Set(collection.value?.packages?.map(p => p.id) || [])];
|
||||||
const newPackageIds = currentPackageIds.filter(id => id !== packageId);
|
const newPackageIds = currentPackageIds.filter(id => id !== packageId);
|
||||||
|
|
||||||
await collectionsApi.setCollectionPackages(route.params.id, newPackageIds);
|
await collectionsApi.setCollectionPackages(route.params.id, newPackageIds);
|
||||||
|
|||||||
@ -274,10 +274,13 @@ const handleSubmit = async () => {
|
|||||||
id = res.id;
|
id = res.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存课程包关联(按 sortOrder 排序)
|
// 保存课程包关联(按 sortOrder 排序,去重避免唯一约束冲突)
|
||||||
if (selectedPackages.value.length > 0) {
|
if (selectedPackages.value.length > 0) {
|
||||||
const sorted = [...selectedPackages.value].sort((a, b) => a.sortOrder - b.sortOrder);
|
const sorted = [...selectedPackages.value].sort((a, b) => a.sortOrder - b.sortOrder);
|
||||||
await setCollectionPackages(id, sorted.map((p) => p.packageId));
|
const packageIds = [...new Map(sorted.map((p) => [p.packageId, p])).values()]
|
||||||
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
|
.map((p) => p.packageId);
|
||||||
|
await setCollectionPackages(id, packageIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success(isEdit.value ? '套餐更新成功' : '套餐创建成功');
|
message.success(isEdit.value ? '套餐更新成功' : '套餐创建成功');
|
||||||
|
|||||||
@ -6,38 +6,25 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-button type="primary" @click="handleCreate">
|
<a-button type="primary" @click="handleCreate">
|
||||||
<template #icon><PlusOutlined /></template>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
新建套餐
|
新建套餐
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 筛选 -->
|
<!-- 筛选 -->
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
<a-select
|
<a-select v-model:value="filters.status" placeholder="状态筛选" style="width: 150px" allowClear @change="fetchData">
|
||||||
v-model:value="filters.status"
|
|
||||||
placeholder="状态筛选"
|
|
||||||
style="width: 150px"
|
|
||||||
allowClear
|
|
||||||
@change="fetchData"
|
|
||||||
>
|
|
||||||
<a-select-option value="DRAFT">草稿</a-select-option>
|
<a-select-option value="DRAFT">草稿</a-select-option>
|
||||||
<a-select-option value="PENDING">待审核</a-select-option>
|
|
||||||
<a-select-option value="APPROVED">已通过</a-select-option>
|
|
||||||
<a-select-option value="PUBLISHED">已发布</a-select-option>
|
<a-select-option value="PUBLISHED">已发布</a-select-option>
|
||||||
<a-select-option value="ARCHIVED">已下架</a-select-option>
|
<a-select-option value="ARCHIVED">已下架</a-select-option>
|
||||||
<a-select-option value="REJECTED">已驳回</a-select-option>
|
|
||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<a-table
|
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="pagination" row-key="id"
|
||||||
:columns="columns"
|
@change="handleTableChange">
|
||||||
:data-source="dataSource"
|
|
||||||
:loading="loading"
|
|
||||||
:pagination="pagination"
|
|
||||||
row-key="id"
|
|
||||||
@change="handleTableChange"
|
|
||||||
>
|
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'price'">
|
<template v-if="column.key === 'price'">
|
||||||
{{ formatPrice(record.price) }}
|
{{ formatPrice(record.price) }}
|
||||||
@ -62,11 +49,7 @@
|
|||||||
<a-button type="primary" size="small" disabled>发布</a-button>
|
<a-button type="primary" size="small" disabled>发布</a-button>
|
||||||
</span>
|
</span>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-popconfirm
|
<a-popconfirm v-else title="确定要发布此套餐吗?发布后学校端将可以查看。" @confirm="handlePublish(record)">
|
||||||
v-else
|
|
||||||
title="确定要发布此套餐吗?发布后学校端将可以查看。"
|
|
||||||
@confirm="handlePublish(record)"
|
|
||||||
>
|
|
||||||
<a-button type="primary" size="small">发布</a-button>
|
<a-button type="primary" size="small">发布</a-button>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
<a-popconfirm title="确定要删除吗?" @confirm="handleDelete(record)">
|
<a-popconfirm title="确定要删除吗?" @confirm="handleDelete(record)">
|
||||||
@ -89,11 +72,7 @@
|
|||||||
<a-button type="primary" size="small" disabled>发布</a-button>
|
<a-button type="primary" size="small" disabled>发布</a-button>
|
||||||
</span>
|
</span>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-popconfirm
|
<a-popconfirm v-else title="确定要发布此套餐吗?发布后学校端将可以查看。" @confirm="handlePublish(record)">
|
||||||
v-else
|
|
||||||
title="确定要发布此套餐吗?发布后学校端将可以查看。"
|
|
||||||
@confirm="handlePublish(record)"
|
|
||||||
>
|
|
||||||
<a-button type="primary" size="small">发布</a-button>
|
<a-button type="primary" size="small">发布</a-button>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</template>
|
</template>
|
||||||
@ -115,9 +94,13 @@
|
|||||||
<!-- 已下架状态 -->
|
<!-- 已下架状态 -->
|
||||||
<template v-else-if="record.status === 'ARCHIVED'">
|
<template v-else-if="record.status === 'ARCHIVED'">
|
||||||
<a-button type="link" size="small" @click="handleView(record)">查看</a-button>
|
<a-button type="link" size="small" @click="handleView(record)">查看</a-button>
|
||||||
|
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||||
<a-popconfirm title="确定要重新发布吗?" @confirm="handleRepublish(record)">
|
<a-popconfirm title="确定要重新发布吗?" @confirm="handleRepublish(record)">
|
||||||
<a-button type="primary" size="small">重新发布</a-button>
|
<a-button type="primary" size="small">重新发布</a-button>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
|
<a-popconfirm title="确定要删除吗?" @confirm="handleDelete(record)">
|
||||||
|
<a-button type="link" size="small" danger>删除</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
</template>
|
</template>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -283,10 +283,13 @@ const handleSave = async () => {
|
|||||||
id = res.id; // 后端 Long 序列化为 string
|
id = res.id; // 后端 Long 序列化为 string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存课程包关联(按 sortOrder 排序)
|
// 保存课程包关联(按 sortOrder 排序,去重避免唯一约束冲突)
|
||||||
if (selectedPackages.value.length > 0) {
|
if (selectedPackages.value.length > 0) {
|
||||||
const sorted = [...selectedPackages.value].sort((a, b) => a.sortOrder - b.sortOrder);
|
const sorted = [...selectedPackages.value].sort((a, b) => a.sortOrder - b.sortOrder);
|
||||||
await setCollectionPackages(id, sorted.map((p) => p.packageId));
|
const packageIds = [...new Map(sorted.map((p) => [p.packageId, p])).values()]
|
||||||
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
|
.map((p) => p.packageId);
|
||||||
|
await setCollectionPackages(id, packageIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
message.success('保存成功');
|
message.success('保存成功');
|
||||||
|
|||||||
@ -169,8 +169,19 @@ const statusTexts: Record<string, string> = {
|
|||||||
const getStatusColor = (status: string) => statusColors[status] || 'default';
|
const getStatusColor = (status: string) => statusColors[status] || 'default';
|
||||||
const getStatusText = (status: string) => statusTexts[status] || status;
|
const getStatusText = (status: string) => statusTexts[status] || status;
|
||||||
|
|
||||||
const parseGradeLevels = (gradeLevels: string | string[]) => {
|
const parseGradeLevels = (gradeLevels: string | string[] | undefined): string[] => {
|
||||||
if (Array.isArray(gradeLevels)) return gradeLevels;
|
if (!gradeLevels) return [];
|
||||||
|
if (Array.isArray(gradeLevels)) {
|
||||||
|
if (gradeLevels.length === 0) return [];
|
||||||
|
if (gradeLevels[0]?.toString().startsWith('[')) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(gradeLevels.join(''));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gradeLevels;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(gradeLevels || '[]');
|
return JSON.parse(gradeLevels || '[]');
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -222,13 +222,16 @@ public class CourseCollectionService extends ServiceImpl<CourseCollectionMapper,
|
|||||||
throw new BusinessException("课程套餐不存在");
|
throw new BusinessException("课程套餐不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 去重:保持首次出现顺序,避免 uk_collection_package 唯一约束冲突
|
||||||
|
List<Long> distinctIds = packageIds.stream().distinct().collect(Collectors.toList());
|
||||||
|
|
||||||
// 验证课程包是否存在(应用层外键约束)
|
// 验证课程包是否存在(应用层外键约束)
|
||||||
if (!packageIds.isEmpty()) {
|
if (!distinctIds.isEmpty()) {
|
||||||
List<CoursePackage> packages = packageMapper.selectList(
|
List<CoursePackage> packages = packageMapper.selectList(
|
||||||
new LambdaQueryWrapper<CoursePackage>()
|
new LambdaQueryWrapper<CoursePackage>()
|
||||||
.in(CoursePackage::getId, packageIds)
|
.in(CoursePackage::getId, distinctIds)
|
||||||
);
|
);
|
||||||
if (packages.size() != packageIds.size()) {
|
if (packages.size() != distinctIds.size()) {
|
||||||
throw new BusinessException("存在无效的课程包 ID");
|
throw new BusinessException("存在无效的课程包 ID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,16 +243,16 @@ public class CourseCollectionService extends ServiceImpl<CourseCollectionMapper,
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 创建新的关联
|
// 创建新的关联
|
||||||
for (int i = 0; i < packageIds.size(); i++) {
|
for (int i = 0; i < distinctIds.size(); i++) {
|
||||||
CourseCollectionPackage association = new CourseCollectionPackage();
|
CourseCollectionPackage association = new CourseCollectionPackage();
|
||||||
association.setCollectionId(collectionId);
|
association.setCollectionId(collectionId);
|
||||||
association.setPackageId(packageIds.get(i));
|
association.setPackageId(distinctIds.get(i));
|
||||||
association.setSortOrder(i + 1);
|
association.setSortOrder(i + 1);
|
||||||
collectionPackageMapper.insert(association);
|
collectionPackageMapper.insert(association);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新课程包数量(复用前面已验证的 collection 变量)
|
// 更新课程包数量(复用前面已验证的 collection 变量)
|
||||||
collection.setPackageCount(packageIds.size());
|
collection.setPackageCount(distinctIds.size());
|
||||||
collectionMapper.updateById(collection);
|
collectionMapper.updateById(collection);
|
||||||
|
|
||||||
log.info("课程套餐的课程包设置完成");
|
log.info("课程套餐的课程包设置完成");
|
||||||
@ -500,6 +503,25 @@ public class CourseCollectionService extends ServiceImpl<CourseCollectionMapper,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析适用年级:数据库存储为 JSON 数组 ["小班","中班","大班"],需正确解析
|
||||||
|
*/
|
||||||
|
private String[] parseGradeLevels(String gradeLevels) {
|
||||||
|
if (!StringUtils.hasText(gradeLevels)) {
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (gradeLevels.trim().startsWith("[")) {
|
||||||
|
return JSON.parseArray(gradeLevels, String.class).toArray(new String[0]);
|
||||||
|
}
|
||||||
|
// 兼容旧数据:逗号分隔格式
|
||||||
|
return gradeLevels.split(",");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("解析 gradeLevels 失败: {}", gradeLevels, e);
|
||||||
|
return new String[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换为响应对象
|
* 转换为响应对象
|
||||||
*/
|
*/
|
||||||
@ -525,7 +547,7 @@ public class CourseCollectionService extends ServiceImpl<CourseCollectionMapper,
|
|||||||
.price(collection.getPrice())
|
.price(collection.getPrice())
|
||||||
.discountPrice(collection.getDiscountPrice())
|
.discountPrice(collection.getDiscountPrice())
|
||||||
.discountType(collection.getDiscountType())
|
.discountType(collection.getDiscountType())
|
||||||
.gradeLevels(collection.getGradeLevels() != null ? collection.getGradeLevels().split(",") : new String[0])
|
.gradeLevels(parseGradeLevels(collection.getGradeLevels()))
|
||||||
.packageCount(collection.getPackageCount())
|
.packageCount(collection.getPackageCount())
|
||||||
.status(collection.getStatus())
|
.status(collection.getStatus())
|
||||||
.submittedAt(collection.getSubmittedAt())
|
.submittedAt(collection.getSubmittedAt())
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user