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.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<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文件
*/
@PostMapping("/import")
public Result<Integer> importExcel(@RequestParam("file") MultipartFile file) {
int count = buildService.importExcel(file);
return Result.success("导入成功", count);
public Result<BuildImportResultVO> importExcel(@RequestParam("file") MultipartFile file) {
BuildImportResultVO result = buildService.importExcel(file);
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.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 {
/**
* 删除教学楼
* <p>
* 已绑定教学场地(教室表存在引用该教学楼代号的未删除记录)时不允许删除。
* </p>
*
* @param id 主键
*/
void delete(String id);
/**
* 批量删除教学楼
* <p>
* 逐条校验教学场地绑定情况:已绑定教学场地的教学楼跳过不删,其余正常删除。
* </p>
*
* @param ids 主键列表
* @return 删除结果(删除/跳过数量与明细)
*/
BuildDeleteResultVO batchDelete(List<String> ids);
/**
* 更新教学楼
*
@@ -62,11 +78,15 @@ public interface BuildService {
/**
* 导入Excel文件
* <p>
* 教学楼代号从模板「教学楼标识号」列读取(原样保留英文字母),留空则自动生成;
* 代号已存在的行跳过,不覆盖库中已有数据。
* </p>
*
* @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.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<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
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<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();
@@ -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);
}
@@ -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
*/
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
@@ -95,7 +95,7 @@
<el-button icon="el-icon-download" :loading="importDialog.downloading" @click="handleDownloadTemplate">
下载导入模板
</el-button>
<span class="tip-text">模板 sheet 教学楼表只需填写教学楼名称/地点/备注</span>
<span class="tip-text">模板 sheet 教学楼表教学楼标识号留空则自动生成代号已存在的行将跳过</span>
</el-form-item>
<el-form-item label="数据文件">
<div class="file-input">
@@ -110,7 +110,12 @@
</el-form-item>
<el-form-item label="导入结果" v-if="importDialog.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>
</el-form-item>
</el-form>
@@ -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 => '<div>' + item + '</div>').join('')
this.$alert(
'已删除 ' + deleteCount + ' 条,' + skipList.length + ' 条因已绑定教学场地未删除。<br/>' + 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;
}
}
}