1、教学楼添加批量删除功能;

2、教学楼导入代号从excel获取,不要自己生成
This commit is contained in:
2026-09-16 09:07:47 +08:00
parent d9b10a3f05
commit b30e39f91a
7 changed files with 336 additions and 35 deletions
@@ -5,6 +5,8 @@ import com.roomroot.jwgl.service.BuildService;
import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageQuery;
import com.roomroot.jwgl.unit.PageResult; import com.roomroot.jwgl.unit.PageResult;
import com.roomroot.jwgl.unit.Result; 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.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -40,6 +42,15 @@ public class BuildController {
return Result.success(); return Result.success();
} }
/**
* 批量删除教学楼(已绑定教学场地的跳过不删)
*/
@PostMapping("/batchDelete")
public Result<BuildDeleteResultVO> batchDelete(@RequestBody List<String> ids) {
BuildDeleteResultVO result = buildService.batchDelete(ids);
return Result.success(result.getMessage(), result);
}
/** /**
* 更新教学楼 * 更新教学楼
*/ */
@@ -77,9 +88,9 @@ public class BuildController {
* 导入Excel文件 * 导入Excel文件
*/ */
@PostMapping("/import") @PostMapping("/import")
public Result<Integer> importExcel(@RequestParam("file") MultipartFile file) { public Result<BuildImportResultVO> importExcel(@RequestParam("file") MultipartFile file) {
int count = buildService.importExcel(file); BuildImportResultVO result = buildService.importExcel(file);
return Result.success("导入成功", count); return Result.success(result.getMessage(), result);
} }
/** /**
@@ -3,6 +3,8 @@ package com.roomroot.jwgl.service;
import com.roomroot.jwgl.entity.JXLB; import com.roomroot.jwgl.entity.JXLB;
import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageQuery;
import com.roomroot.jwgl.unit.PageResult; 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 jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -23,11 +25,25 @@ public interface BuildService {
/** /**
* 删除教学楼 * 删除教学楼
* <p>
* 已绑定教学场地(教室表存在引用该教学楼代号的未删除记录)时不允许删除。
* </p>
* *
* @param id 主键 * @param id 主键
*/ */
void delete(String id); void delete(String id);
/**
* 批量删除教学楼
* <p>
* 逐条校验教学场地绑定情况:已绑定教学场地的教学楼跳过不删,其余正常删除。
* </p>
*
* @param ids 主键列表
* @return 删除结果(删除/跳过数量与明细)
*/
BuildDeleteResultVO batchDelete(List<String> ids);
/** /**
* 更新教学楼 * 更新教学楼
* *
@@ -62,11 +78,15 @@ public interface BuildService {
/** /**
* 导入Excel文件 * 导入Excel文件
* <p>
* 教学楼代号从模板「教学楼标识号」列读取(原样保留英文字母),留空则自动生成;
* 代号已存在的行跳过,不覆盖库中已有数据。
* </p>
* *
* @param file Excel文件 * @param file Excel文件
* @return 导入数量 * @return 导入结果(新增/跳过数量与明细)
*/ */
int importExcel(MultipartFile file); BuildImportResultVO importExcel(MultipartFile file);
/** /**
* 下载模板文件 * 下载模板文件
@@ -2,17 +2,24 @@ package com.roomroot.jwgl.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.entity.JXLB;
import com.roomroot.jwgl.mapper.BuildMapper; import com.roomroot.jwgl.mapper.BuildMapper;
import com.roomroot.jwgl.mapper.ClassRoomMapper;
import com.roomroot.jwgl.service.BuildService; import com.roomroot.jwgl.service.BuildService;
import com.roomroot.jwgl.unit.BusinessException;
import com.roomroot.jwgl.unit.PageQuery; import com.roomroot.jwgl.unit.PageQuery;
import com.roomroot.jwgl.unit.PageResult; import com.roomroot.jwgl.unit.PageResult;
import com.roomroot.jwgl.utils.UuidUtil; 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.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -20,7 +27,12 @@ import jakarta.servlet.http.HttpServletResponse;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; 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 @Resource
private BuildMapper buildMapper; private BuildMapper buildMapper;
@Resource
private ClassRoomMapper classRoomMapper;
@Override @Override
public void add(JXLB jxlb) { public void add(JXLB jxlb) {
String uuid = UuidUtil.getUUID(); String uuid = UuidUtil.getUUID();
@@ -49,10 +64,80 @@ public class BuildServiceImpl implements BuildService {
@Override @Override
public void delete(String id) { public void delete(String id) {
JXLB jxlb = getById(id); JXLB jxlb = getById(id);
if (jxlb == null) {
return;
}
assertNotBound(jxlb);
jxlb.setDelFlag(1); jxlb.setDelFlag(1);
buildMapper.updateById(jxlb); buildMapper.updateById(jxlb);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public BuildDeleteResultVO batchDelete(List<String> ids) {
BuildDeleteResultVO result = new BuildDeleteResultVO();
if (ids == null || ids.isEmpty()) {
result.setMessage("未选择要删除的教学楼");
return result;
}
List<JXLB> builds = buildMapper.selectBatchIds(ids);
// 一次性查出这些教学楼代号下未删除的教学场地,按代号分组计数
Set<String> codeSet = builds.stream()
.map(JXLB::getJxldh)
.filter(StringUtils::isNotEmpty)
.map(String::trim)
.collect(Collectors.toSet());
Map<String, Long> boundCountMap = codeSet.isEmpty()
? new HashMap<>()
: classRoomMapper.selectList(new LambdaQueryWrapper<JSB>()
.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<JSB>()
.eq(JSB::getDelFlag, 0)
.eq(JSB::getJxldh, jxlb.getJxldh().trim()));
if (boundCount != null && boundCount > 0) {
throw new BusinessException("教学楼 " + jxlb.getJxlmc() + "(代号 " + jxlb.getJxldh()
+ ")已绑定 " + boundCount + " 间教学场地,不允许删除");
}
}
@Override @Override
public void update(JXLB jxlb) { public void update(JXLB jxlb) {
buildMapper.updateById(jxlb); buildMapper.updateById(jxlb);
@@ -93,13 +178,29 @@ public class BuildServiceImpl implements BuildService {
} }
@Override @Override
public int importExcel(MultipartFile file) { public BuildImportResultVO importExcel(MultipartFile file) {
try (InputStream is = file.getInputStream(); try (InputStream is = file.getInputStream();
Workbook workbook = file.getOriginalFilename().endsWith(".xlsx") Workbook workbook = file.getOriginalFilename().endsWith(".xlsx")
? new XSSFWorkbook(is) : new HSSFWorkbook(is)) { ? new XSSFWorkbook(is) : new HSSFWorkbook(is)) {
Sheet sheet = workbook.getSheetAt(0); // 模板数据写在「教学楼表」sheet,取不到时兜底取第一个 sheet
int count = 0; Sheet sheet = workbook.getSheet("教学楼表");
if (sheet == null) {
sheet = workbook.getSheetAt(0);
}
BuildImportResultVO result = new BuildImportResultVO();
// 库中已有教学楼代号集合,导入命中即跳过(不覆盖已有数据);
// 导入中新写入的代号也加入集合,避免同一文件内代号重复
Set<String> existingJxldh = new HashSet<>();
for (JXLB existing : buildMapper.selectList(new LambdaQueryWrapper<JXLB>()
.select(JXLB::getJxldh)
.eq(JXLB::getDelFlag, 0))) {
if (StringUtils.isNotEmpty(existing.getJxldh())) {
existingJxldh.add(existing.getJxldh().trim());
}
}
// 在循环外获取起始序号 // 在循环外获取起始序号
int nextNum = getNextNumber(); int nextNum = getNextNumber();
@@ -109,29 +210,56 @@ public class BuildServiceImpl implements BuildService {
Row row = sheet.getRow(i); Row row = sheet.getRow(i);
if (row == null) continue; 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)); if (StringUtils.isEmpty(jxldh) && StringUtils.isEmpty(xh)
jxlb.setJxldh(String.format("%03d", nextNum)); && 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++; // 递增序号 nextNum++; // 递增序号
// 教学楼名称 jxlb.setJxlmc(jxlmc);
jxlb.setJxlmc(getCellValue(row, 2)); jxlb.setDd(dd);
jxlb.setBz(bz);
// 地点
jxlb.setDd(getCellValue(row, 3));
// 备注
jxlb.setBz(getCellValue(row, 4));
jxlb.setId(UuidUtil.getUUID()); jxlb.setId(UuidUtil.getUUID());
jxlb.setDelFlag(0); jxlb.setDelFlag(0);
buildMapper.insert(jxlb); 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) { } catch (Exception e) {
throw new RuntimeException("Excel解析失败: " + e.getMessage(), e); throw new RuntimeException("Excel解析失败: " + e.getMessage(), e);
} }
@@ -0,0 +1,48 @@
package com.roomroot.jwgl.vo.build;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 教学楼批量删除结果。
* <p>
* 用于返回批量删除的执行结果:删除数、跳过数(已绑定教学场地不允许删除)与跳过明细。
* </p>
*/
@Data
public class BuildDeleteResultVO {
/**
* 实际删除的教学楼数。
*/
private Integer deleteCount = 0;
/**
* 因"已绑定教学场地"而跳过的条数。
*/
private Integer skipCount = 0;
/**
* 跳过明细(教学楼 XXX(代号 XXX)已绑定教学场地,未删除)。
*/
private List<String> skipList = new ArrayList<>();
/**
* 结果提示信息。
*/
private String message;
/**
* 记录一条跳过明细。
*
* @param detail 明细文本
*/
public void addSkip(String detail) {
this.skipCount++;
if (this.skipList.size() < 50) {
this.skipList.add(detail);
}
}
}
@@ -0,0 +1,48 @@
package com.roomroot.jwgl.vo.build;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 教学楼导入结果。
* <p>
* 用于返回导入操作的执行结果:新增数、跳过数(教学楼代号已存在)与跳过明细。
* </p>
*/
@Data
public class BuildImportResultVO {
/**
* 实际新增的教学楼数。
*/
private Integer insertCount = 0;
/**
* 因"教学楼代号已存在"而跳过的条数(不覆盖库中已有数据)。
*/
private Integer skipCount = 0;
/**
* 跳过明细(第 N 行:教学楼代号 XXX 已存在)。
*/
private List<String> skipList = new ArrayList<>();
/**
* 结果提示信息。
*/
private String message;
/**
* 记录一条跳过明细。
*
* @param detail 明细文本
*/
public void addSkip(String detail) {
this.skipCount++;
if (this.skipList.size() < 50) {
this.skipList.add(detail);
}
}
}
+15 -1
View File
@@ -17,7 +17,7 @@ export function addBuild(data) {
} }
/** /**
* 删除教学楼(后端仅提供单条删除,参数为 id) * 删除教学楼(参数为 id,已绑定教学场地时后端会拒绝)
* POST /build/delete * POST /build/delete
*/ */
export function deleteBuild(id) { 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 * POST /build/update
@@ -95,7 +95,7 @@
<el-button icon="el-icon-download" :loading="importDialog.downloading" @click="handleDownloadTemplate"> <el-button icon="el-icon-download" :loading="importDialog.downloading" @click="handleDownloadTemplate">
下载导入模板 下载导入模板
</el-button> </el-button>
<span class="tip-text">模板 sheet 为「教学楼表」,只需填写教学楼名称/地点/备注</span> <span class="tip-text">模板 sheet 为「教学楼表」,教学楼标识号留空则自动生成,代号已存在的行将跳过</span>
</el-form-item> </el-form-item>
<el-form-item label="数据文件"> <el-form-item label="数据文件">
<div class="file-input"> <div class="file-input">
@@ -110,7 +110,12 @@
</el-form-item> </el-form-item>
<el-form-item label="导入结果" v-if="importDialog.result"> <el-form-item label="导入结果" v-if="importDialog.result">
<div class="import-result"> <div class="import-result">
共导入 <span class="num">{{ importDialog.result }}</span> 条教学楼数据。 共导入 <span class="num">{{ importDialog.result.insertCount }}</span> 条教学楼数据<template
v-if="importDialog.result.skipCount > 0">,跳过 <span
class="num warn">{{ importDialog.result.skipCount }}</span> 条(教学楼代号已存在)</template>。
<div v-if="importDialog.result.skipList && importDialog.result.skipList.length" class="skip-detail">
<div v-for="(item, idx) in importDialog.result.skipList" :key="idx">{{ item }}</div>
</div>
</div> </div>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -127,7 +132,7 @@
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import { import {
addBuild, addBuild,
deleteBuild, batchDeleteBuild,
updateBuild, updateBuild,
listBuild, listBuild,
importBuild, importBuild,
@@ -318,7 +323,7 @@ export default {
}) })
}, },
/* ---------- 删除(后端仅提供单条删除,多选时逐条调用) ---------- */ /* ---------- 删除(批量删除,已绑定教学场地的由后端跳过) ---------- */
handleBatchDelete() { handleBatchDelete() {
const rows = this.selectedRows || [] const rows = this.selectedRows || []
if (rows.length === 0) { if (rows.length === 0) {
@@ -326,7 +331,7 @@ export default {
return return
} }
const names = rows.map(row => row.jxlmc || row.jxldh).join('、') const names = rows.map(row => row.jxlmc || row.jxldh).join('、')
this.$confirm('确定删除选中的 ' + rows.length + ' 条教学楼数据吗?' + '(' + names + ')', '提示', { this.$confirm('确定删除选中的 ' + rows.length + ' 条教学楼数据吗?' + '(' + names + ')已绑定教学场地的教学楼不会被删除。', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
@@ -335,13 +340,23 @@ export default {
doBatchDelete(rows) { doBatchDelete(rows) {
this.loading = true this.loading = true
const tasks = rows.map(row => deleteBuild(row.id)) batchDeleteBuild(rows.map(row => row.id)).then(response => {
Promise.all(tasks).then(() => { const data = (response && response.data) || {}
this.$message.success('已删除 ' + rows.length + ' 条教学楼数据') const deleteCount = data.deleteCount || 0
const skipList = data.skipList || []
this.loading = false this.loading = false
if (skipList.length > 0) {
const detailHtml = skipList.map(item => '<div>' + item + '</div>').join('')
this.$alert(
'已删除 ' + deleteCount + ' 条,' + skipList.length + ' 条因已绑定教学场地未删除。<br/>' + detailHtml,
'删除结果',
{ dangerouslyUseHTMLString: true, confirmButtonText: '确定' }
)
} else {
this.$message.success('已删除 ' + deleteCount + ' 条教学楼数据')
}
this.fetchList() this.fetchList()
}).catch(() => { }).catch(() => {
this.$message.error('部分数据删除失败,请刷新后重试')
this.loading = false this.loading = false
this.fetchList() this.fetchList()
}) })
@@ -406,11 +421,16 @@ export default {
} }
this.importDialog.importing = true this.importDialog.importing = true
importBuild(this.importDialog.file).then(response => { importBuild(this.importDialog.file).then(response => {
const count = (response && response.data) || 0 const data = (response && response.data) || {}
this.clearImportFile() this.clearImportFile()
this.importDialog.importing = false this.importDialog.importing = false
this.importDialog.result = count this.importDialog.result = {
this.$message.success('导入成功,共导入 ' + count + ' 条') 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() this.fetchList()
}).catch(() => { }).catch(() => {
this.importDialog.importing = false this.importDialog.importing = false
@@ -492,6 +512,18 @@ export default {
font-weight: 600; font-weight: 600;
color: #67c23a; color: #67c23a;
padding: 0 4px; padding: 0 4px;
&.warn {
color: #e6a23c;
}
}
.skip-detail {
margin-top: 8px;
font-size: 13px;
color: #909399;
max-height: 120px;
overflow-y: auto;
} }
} }
} }