diff --git a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/BuildController.java b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/BuildController.java index 4ed1955cb..8f68e1175 100644 --- a/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/BuildController.java +++ b/backend/roomroot-admin/src/main/java/com/roomroot/web/controller/jwgl/BuildController.java @@ -5,6 +5,8 @@ import com.roomroot.jwgl.service.BuildService; import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageResult; import com.roomroot.jwgl.unit.Result; +import com.roomroot.jwgl.vo.build.BuildDeleteResultVO; +import com.roomroot.jwgl.vo.build.BuildImportResultVO; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.*; @@ -40,6 +42,15 @@ public class BuildController { return Result.success(); } + /** + * 批量删除教学楼(已绑定教学场地的跳过不删) + */ + @PostMapping("/batchDelete") + public Result batchDelete(@RequestBody List ids) { + BuildDeleteResultVO result = buildService.batchDelete(ids); + return Result.success(result.getMessage(), result); + } + /** * 更新教学楼 */ @@ -77,9 +88,9 @@ public class BuildController { * 导入Excel文件 */ @PostMapping("/import") - public Result importExcel(@RequestParam("file") MultipartFile file) { - int count = buildService.importExcel(file); - return Result.success("导入成功", count); + public Result importExcel(@RequestParam("file") MultipartFile file) { + BuildImportResultVO result = buildService.importExcel(file); + return Result.success(result.getMessage(), result); } /** diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/BuildService.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/BuildService.java index 906ad8fa1..11d053b3e 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/BuildService.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/BuildService.java @@ -3,6 +3,8 @@ package com.roomroot.jwgl.service; import com.roomroot.jwgl.entity.JXLB; import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageResult; +import com.roomroot.jwgl.vo.build.BuildDeleteResultVO; +import com.roomroot.jwgl.vo.build.BuildImportResultVO; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; @@ -23,11 +25,25 @@ public interface BuildService { /** * 删除教学楼 + *

+ * 已绑定教学场地(教室表存在引用该教学楼代号的未删除记录)时不允许删除。 + *

* * @param id 主键 */ void delete(String id); + /** + * 批量删除教学楼 + *

+ * 逐条校验教学场地绑定情况:已绑定教学场地的教学楼跳过不删,其余正常删除。 + *

+ * + * @param ids 主键列表 + * @return 删除结果(删除/跳过数量与明细) + */ + BuildDeleteResultVO batchDelete(List ids); + /** * 更新教学楼 * @@ -62,11 +78,15 @@ public interface BuildService { /** * 导入Excel文件 + *

+ * 教学楼代号从模板「教学楼标识号」列读取(原样保留英文字母),留空则自动生成; + * 代号已存在的行跳过,不覆盖库中已有数据。 + *

* * @param file Excel文件 - * @return 导入数量 + * @return 导入结果(新增/跳过数量与明细) */ - int importExcel(MultipartFile file); + BuildImportResultVO importExcel(MultipartFile file); /** * 下载模板文件 diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/BuildServiceImpl.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/BuildServiceImpl.java index c9b097b97..0abd2a34f 100644 --- a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/BuildServiceImpl.java +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/service/impl/BuildServiceImpl.java @@ -2,17 +2,24 @@ package com.roomroot.jwgl.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.roomroot.common.utils.StringUtils; +import com.roomroot.jwgl.entity.JSB; import com.roomroot.jwgl.entity.JXLB; import com.roomroot.jwgl.mapper.BuildMapper; +import com.roomroot.jwgl.mapper.ClassRoomMapper; import com.roomroot.jwgl.service.BuildService; +import com.roomroot.jwgl.unit.BusinessException; import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageResult; import com.roomroot.jwgl.utils.UuidUtil; +import com.roomroot.jwgl.vo.build.BuildDeleteResultVO; +import com.roomroot.jwgl.vo.build.BuildImportResultVO; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import jakarta.annotation.Resource; @@ -20,7 +27,12 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.InputStream; import java.io.OutputStream; import java.net.URLEncoder; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * 教学楼表服务实现类 @@ -31,6 +43,9 @@ public class BuildServiceImpl implements BuildService { @Resource private BuildMapper buildMapper; + @Resource + private ClassRoomMapper classRoomMapper; + @Override public void add(JXLB jxlb) { String uuid = UuidUtil.getUUID(); @@ -49,10 +64,80 @@ public class BuildServiceImpl implements BuildService { @Override public void delete(String id) { JXLB jxlb = getById(id); + if (jxlb == null) { + return; + } + assertNotBound(jxlb); jxlb.setDelFlag(1); buildMapper.updateById(jxlb); } + @Override + @Transactional(rollbackFor = Exception.class) + public BuildDeleteResultVO batchDelete(List ids) { + BuildDeleteResultVO result = new BuildDeleteResultVO(); + if (ids == null || ids.isEmpty()) { + result.setMessage("未选择要删除的教学楼"); + return result; + } + + List builds = buildMapper.selectBatchIds(ids); + + // 一次性查出这些教学楼代号下未删除的教学场地,按代号分组计数 + Set codeSet = builds.stream() + .map(JXLB::getJxldh) + .filter(StringUtils::isNotEmpty) + .map(String::trim) + .collect(Collectors.toSet()); + Map boundCountMap = codeSet.isEmpty() + ? new HashMap<>() + : classRoomMapper.selectList(new LambdaQueryWrapper() + .select(JSB::getJxldh) + .eq(JSB::getDelFlag, 0) + .in(JSB::getJxldh, codeSet)) + .stream() + .filter(jsb -> StringUtils.isNotEmpty(jsb.getJxldh())) + .collect(Collectors.groupingBy(jsb -> jsb.getJxldh().trim(), Collectors.counting())); + + for (JXLB jxlb : builds) { + // 已删除的不再处理 + if (jxlb.getDelFlag() != null && jxlb.getDelFlag() == 1) { + continue; + } + // 已绑定教学场地的教学楼跳过,不允许删除 + long boundCount = StringUtils.isEmpty(jxlb.getJxldh()) + ? 0 + : boundCountMap.getOrDefault(jxlb.getJxldh().trim(), 0L); + if (boundCount > 0) { + result.addSkip("教学楼 " + jxlb.getJxlmc() + "(代号 " + jxlb.getJxldh() + + ")已绑定 " + boundCount + " 间教学场地,未删除"); + continue; + } + jxlb.setDelFlag(1); + buildMapper.updateById(jxlb); + result.setDeleteCount(result.getDeleteCount() + 1); + } + result.setMessage(String.format("已删除 %d 条,跳过 %d 条", + result.getDeleteCount(), result.getSkipCount())); + return result; + } + + /** + * 校验教学楼未绑定教学场地(教室表),已绑定则抛出业务异常 + */ + private void assertNotBound(JXLB jxlb) { + if (StringUtils.isEmpty(jxlb.getJxldh())) { + return; + } + Long boundCount = classRoomMapper.selectCount(new LambdaQueryWrapper() + .eq(JSB::getDelFlag, 0) + .eq(JSB::getJxldh, jxlb.getJxldh().trim())); + if (boundCount != null && boundCount > 0) { + throw new BusinessException("教学楼 " + jxlb.getJxlmc() + "(代号 " + jxlb.getJxldh() + + ")已绑定 " + boundCount + " 间教学场地,不允许删除"); + } + } + @Override public void update(JXLB jxlb) { buildMapper.updateById(jxlb); @@ -93,13 +178,29 @@ public class BuildServiceImpl implements BuildService { } @Override - public int importExcel(MultipartFile file) { + public BuildImportResultVO importExcel(MultipartFile file) { try (InputStream is = file.getInputStream(); Workbook workbook = file.getOriginalFilename().endsWith(".xlsx") ? new XSSFWorkbook(is) : new HSSFWorkbook(is)) { - Sheet sheet = workbook.getSheetAt(0); - int count = 0; + // 模板数据写在「教学楼表」sheet,取不到时兜底取第一个 sheet + Sheet sheet = workbook.getSheet("教学楼表"); + if (sheet == null) { + sheet = workbook.getSheetAt(0); + } + + BuildImportResultVO result = new BuildImportResultVO(); + + // 库中已有教学楼代号集合,导入命中即跳过(不覆盖已有数据); + // 导入中新写入的代号也加入集合,避免同一文件内代号重复 + Set existingJxldh = new HashSet<>(); + for (JXLB existing : buildMapper.selectList(new LambdaQueryWrapper() + .select(JXLB::getJxldh) + .eq(JXLB::getDelFlag, 0))) { + if (StringUtils.isNotEmpty(existing.getJxldh())) { + existingJxldh.add(existing.getJxldh().trim()); + } + } // 在循环外获取起始序号 int nextNum = getNextNumber(); @@ -109,29 +210,56 @@ public class BuildServiceImpl implements BuildService { Row row = sheet.getRow(i); if (row == null) continue; - JXLB jxlb = new JXLB(); + // 教学楼标识号(代号,可含英文字母,原样保留) + String jxldh = getCellValue(row, 0); + // 序号 + String xh = getCellValue(row, 1); + // 教学楼名称 + String jxlmc = getCellValue(row, 2); + // 地点 + String dd = getCellValue(row, 3); + // 备注 + String bz = getCellValue(row, 4); - // 生成序号(数字字符串)和教学楼代号(三位补零) - jxlb.setXh(String.valueOf(nextNum)); - jxlb.setJxldh(String.format("%03d", nextNum)); + // 整行空白则跳过 + if (StringUtils.isEmpty(jxldh) && StringUtils.isEmpty(xh) + && StringUtils.isEmpty(jxlmc) && StringUtils.isEmpty(dd) + && StringUtils.isEmpty(bz)) { + continue; + } + + if (StringUtils.isEmpty(jxldh)) { + // 代号留空时按现有最大序号 +1 自动生成三位补零编号,已占用则继续向后取号 + jxldh = String.format("%03d", nextNum); + while (existingJxldh.contains(jxldh)) { + jxldh = String.format("%03d", ++nextNum); + } + } else if (existingJxldh.contains(jxldh)) { + // 填写的代号已存在则跳过该行 + result.addSkip("第" + (i + 1) + "行:教学楼代号 " + jxldh + " 已存在,已跳过"); + continue; + } + + JXLB jxlb = new JXLB(); + // 序号留空时按自增序号生成 + jxlb.setXh(StringUtils.isEmpty(xh) ? String.valueOf(nextNum) : xh); + jxlb.setJxldh(jxldh); nextNum++; // 递增序号 - // 教学楼名称 - jxlb.setJxlmc(getCellValue(row, 2)); - - // 地点 - jxlb.setDd(getCellValue(row, 3)); - - // 备注 - jxlb.setBz(getCellValue(row, 4)); + jxlb.setJxlmc(jxlmc); + jxlb.setDd(dd); + jxlb.setBz(bz); jxlb.setId(UuidUtil.getUUID()); jxlb.setDelFlag(0); buildMapper.insert(jxlb); - count++; + existingJxldh.add(jxldh); + result.setInsertCount(result.getInsertCount() + 1); } - return count; + result.setMessage(String.format("共导入 %d 条,跳过 %d 条", + result.getInsertCount(), result.getSkipCount())); + return result; } catch (Exception e) { throw new RuntimeException("Excel解析失败: " + e.getMessage(), e); } diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/build/BuildDeleteResultVO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/build/BuildDeleteResultVO.java new file mode 100644 index 000000000..de5f28c1a --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/build/BuildDeleteResultVO.java @@ -0,0 +1,48 @@ +package com.roomroot.jwgl.vo.build; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 教学楼批量删除结果。 + *

+ * 用于返回批量删除的执行结果:删除数、跳过数(已绑定教学场地不允许删除)与跳过明细。 + *

+ */ +@Data +public class BuildDeleteResultVO { + + /** + * 实际删除的教学楼数。 + */ + private Integer deleteCount = 0; + + /** + * 因"已绑定教学场地"而跳过的条数。 + */ + private Integer skipCount = 0; + + /** + * 跳过明细(教学楼 XXX(代号 XXX)已绑定教学场地,未删除)。 + */ + private List skipList = new ArrayList<>(); + + /** + * 结果提示信息。 + */ + private String message; + + /** + * 记录一条跳过明细。 + * + * @param detail 明细文本 + */ + public void addSkip(String detail) { + this.skipCount++; + if (this.skipList.size() < 50) { + this.skipList.add(detail); + } + } +} diff --git a/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/build/BuildImportResultVO.java b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/build/BuildImportResultVO.java new file mode 100644 index 000000000..5c815c591 --- /dev/null +++ b/backend/roomroot-jwgl/src/main/java/com/roomroot/jwgl/vo/build/BuildImportResultVO.java @@ -0,0 +1,48 @@ +package com.roomroot.jwgl.vo.build; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 教学楼导入结果。 + *

+ * 用于返回导入操作的执行结果:新增数、跳过数(教学楼代号已存在)与跳过明细。 + *

+ */ +@Data +public class BuildImportResultVO { + + /** + * 实际新增的教学楼数。 + */ + private Integer insertCount = 0; + + /** + * 因"教学楼代号已存在"而跳过的条数(不覆盖库中已有数据)。 + */ + private Integer skipCount = 0; + + /** + * 跳过明细(第 N 行:教学楼代号 XXX 已存在)。 + */ + private List skipList = new ArrayList<>(); + + /** + * 结果提示信息。 + */ + private String message; + + /** + * 记录一条跳过明细。 + * + * @param detail 明细文本 + */ + public void addSkip(String detail) { + this.skipCount++; + if (this.skipList.size() < 50) { + this.skipList.add(detail); + } + } +} diff --git a/frontend/src/api/teachBusiness/build.js b/frontend/src/api/teachBusiness/build.js index d7129b675..3c2104363 100644 --- a/frontend/src/api/teachBusiness/build.js +++ b/frontend/src/api/teachBusiness/build.js @@ -17,7 +17,7 @@ export function addBuild(data) { } /** - * 删除教学楼(后端仅提供单条删除,参数为 id) + * 删除教学楼(参数为 id,已绑定教学场地时后端会拒绝) * POST /build/delete */ export function deleteBuild(id) { @@ -28,6 +28,20 @@ export function deleteBuild(id) { }) } +/** + * 批量删除教学楼 + * POST /build/batchDelete + * 已绑定教学场地的教学楼会跳过不删,返回 { deleteCount, skipCount, skipList, message } + * @param ids 教学楼ID数组 + */ +export function batchDeleteBuild(ids) { + return request({ + url: '/build/batchDelete', + method: 'post', + data: ids + }) +} + /** * 更新教学楼 * POST /build/update diff --git a/frontend/src/views/teachBusiness/build/index.vue b/frontend/src/views/teachBusiness/build/index.vue index 1217b2088..1b4c2c183 100644 --- a/frontend/src/views/teachBusiness/build/index.vue +++ b/frontend/src/views/teachBusiness/build/index.vue @@ -95,7 +95,7 @@ 下载导入模板 - 模板 sheet 为「教学楼表」,只需填写教学楼名称/地点/备注 + 模板 sheet 为「教学楼表」,教学楼标识号留空则自动生成,代号已存在的行将跳过
@@ -110,7 +110,12 @@
- 共导入 {{ importDialog.result }} 条教学楼数据。 + 共导入 {{ importDialog.result.insertCount }} 条教学楼数据。 +
+
{{ item }}
+
@@ -127,7 +132,7 @@ import { saveAs } from 'file-saver' import { addBuild, - deleteBuild, + batchDeleteBuild, updateBuild, listBuild, importBuild, @@ -318,7 +323,7 @@ export default { }) }, - /* ---------- 删除(后端仅提供单条删除,多选时逐条调用) ---------- */ + /* ---------- 删除(批量删除,已绑定教学场地的由后端跳过) ---------- */ handleBatchDelete() { const rows = this.selectedRows || [] if (rows.length === 0) { @@ -326,7 +331,7 @@ export default { return } const names = rows.map(row => row.jxlmc || row.jxldh).join('、') - this.$confirm('确定删除选中的 ' + rows.length + ' 条教学楼数据吗?' + '(' + names + ')', '提示', { + this.$confirm('确定删除选中的 ' + rows.length + ' 条教学楼数据吗?' + '(' + names + ')已绑定教学场地的教学楼不会被删除。', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' @@ -335,13 +340,23 @@ export default { doBatchDelete(rows) { this.loading = true - const tasks = rows.map(row => deleteBuild(row.id)) - Promise.all(tasks).then(() => { - this.$message.success('已删除 ' + rows.length + ' 条教学楼数据') + batchDeleteBuild(rows.map(row => row.id)).then(response => { + const data = (response && response.data) || {} + const deleteCount = data.deleteCount || 0 + const skipList = data.skipList || [] this.loading = false + if (skipList.length > 0) { + const detailHtml = skipList.map(item => '
' + item + '
').join('') + this.$alert( + '已删除 ' + deleteCount + ' 条,' + skipList.length + ' 条因已绑定教学场地未删除。
' + detailHtml, + '删除结果', + { dangerouslyUseHTMLString: true, confirmButtonText: '确定' } + ) + } else { + this.$message.success('已删除 ' + deleteCount + ' 条教学楼数据') + } this.fetchList() }).catch(() => { - this.$message.error('部分数据删除失败,请刷新后重试') this.loading = false this.fetchList() }) @@ -406,11 +421,16 @@ export default { } this.importDialog.importing = true importBuild(this.importDialog.file).then(response => { - const count = (response && response.data) || 0 + const data = (response && response.data) || {} this.clearImportFile() this.importDialog.importing = false - this.importDialog.result = count - this.$message.success('导入成功,共导入 ' + count + ' 条') + this.importDialog.result = { + insertCount: data.insertCount || 0, + skipCount: data.skipCount || 0, + skipList: data.skipList || [] + } + const skipTip = data.skipCount > 0 ? ',跳过 ' + data.skipCount + ' 条已存在代号' : '' + this.$message.success('导入成功,共导入 ' + (data.insertCount || 0) + ' 条' + skipTip) this.fetchList() }).catch(() => { this.importDialog.importing = false @@ -492,6 +512,18 @@ export default { font-weight: 600; color: #67c23a; padding: 0 4px; + + &.warn { + color: #e6a23c; + } + } + + .skip-detail { + margin-top: 8px; + font-size: 13px; + color: #909399; + max-height: 120px; + overflow-y: auto; } } }