diff --git a/.gitignore b/.gitignore index ff490a0..ce1d174 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ Thumbs.db .trae/pr-creator/SKILL.md .trae/update-docs/SKILL.md .trae/webapp-testing/SKILL.md +outputs \ No newline at end of file diff --git a/outputs/therapy_travel_agency_20261026/旅行社导入模板.xlsx b/outputs/therapy_travel_agency_20261026/旅行社导入模板.xlsx new file mode 100644 index 0000000..2a4c51e Binary files /dev/null and b/outputs/therapy_travel_agency_20261026/旅行社导入模板.xlsx differ diff --git a/src/main/java/com/budwk/app/web/commons/ext/websocket/WkWebSocket.java b/src/main/java/com/budwk/app/web/commons/ext/websocket/WkWebSocket.java index cafd370..ed6df5a 100644 --- a/src/main/java/com/budwk/app/web/commons/ext/websocket/WkWebSocket.java +++ b/src/main/java/com/budwk/app/web/commons/ext/websocket/WkWebSocket.java @@ -10,7 +10,6 @@ import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.log.Log; import org.nutz.log.Logs; import org.nutz.plugins.mvc.websocket.AbstractWsEndpoint; -import org.nutz.plugins.mvc.websocket.NutWsConfigurator; import org.nutz.plugins.mvc.websocket.WsHandler; import redis.clients.jedis.*; @@ -21,7 +20,7 @@ import javax.websocket.server.ServerEndpoint; import java.util.ArrayList; import java.util.List; -@ServerEndpoint(value = "/websocket", configurator = NutWsConfigurator.class) +@ServerEndpoint(value = "/websocket", configurator = WkWsConfigurator.class) @IocBean(create = "init") // 使用NutWsConfigurator的必备条件 public class WkWebSocket extends AbstractWsEndpoint implements PubSub { protected static final Log log = Logs.get(); diff --git a/src/main/java/com/budwk/app/web/commons/ext/websocket/WkWsConfigurator.java b/src/main/java/com/budwk/app/web/commons/ext/websocket/WkWsConfigurator.java new file mode 100644 index 0000000..8662f62 --- /dev/null +++ b/src/main/java/com/budwk/app/web/commons/ext/websocket/WkWsConfigurator.java @@ -0,0 +1,20 @@ +package com.budwk.app.web.commons.ext.websocket; + +import org.nutz.plugins.mvc.websocket.NutWsConfigurator; + +import javax.websocket.HandshakeResponse; +import javax.websocket.server.HandshakeRequest; +import javax.websocket.server.ServerEndpointConfig; + +public class WkWsConfigurator extends NutWsConfigurator { + @Override + public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) { + // WebSocket升级不一定经过Nutz请求链,只使用当前握手的会话,避免读取已回收的MVC请求。 + Object session = request.getHttpSession(); + if (session == null) { + config.getUserProperties().remove("HttpSession"); + } else { + config.getUserProperties().put("HttpSession", session); + } + } +} diff --git a/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/TherapyWorkspaceController.java b/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/TherapyWorkspaceController.java index b6cbf33..11e98ca 100644 --- a/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/TherapyWorkspaceController.java +++ b/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/TherapyWorkspaceController.java @@ -42,7 +42,7 @@ public class TherapyWorkspaceController { if (!found) throw new IllegalArgumentException("未知的普惠疗休养V3页面"); } List tabs = new ArrayList<>(); - String[][] groups = {{"line", "base"}, {"overview", "cluster", "adjustment"}, + String[][] groups = {{"overview", "cluster", "adjustment"}, {"branchQuery", "branchAudit", "travelAudit"}, {"userQuery", "schoolAudit", "import"}}; for (String[] group : groups) { if (Arrays.asList(group).contains(screen)) { diff --git a/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/basicManage/TheRapyRecuperationLineController.java b/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/basicManage/TheRapyRecuperationLineController.java index aa17112..d7ddab1 100644 --- a/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/basicManage/TheRapyRecuperationLineController.java +++ b/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/basicManage/TheRapyRecuperationLineController.java @@ -10,6 +10,8 @@ import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.SysOfficeTemplateUtil; +import com.budwk.app.sys.models.Sys_office_template; import com.budwk.app.zhgh.staffbenefit.therapyRecuperation.support.ViReturn; import com.budwk.app.zhgh.staffbenefit.therapyRecuperation.support.TherapyContext; import com.budwk.app.base.param.PageForm; @@ -66,6 +68,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; +import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; @@ -104,6 +107,23 @@ public class TheRapyRecuperationLineController { private TheRapyRecuperationLineService lineService; @Inject private Dao dao; + @Inject + private SysOfficeTemplateUtil sysOfficeTemplateUtil; + + @At + @Ok("void") + @SaCheckPermission("inclusiveTherapyV3.line") + public void downloadTemplate(HttpServletResponse response) throws IOException { + String templateCode = "inclusive_therapy_v3_line_import"; + try (InputStream in = sysOfficeTemplateUtil.getTemplate(templateCode)) { + Sys_office_template template = dao.fetch(Sys_office_template.class, Cnd.where("templateCode", "=", templateCode)); + TherapyDownload.excelResponse(response, template.getTemplateName()); + if (template.getTemplateName().toLowerCase(Locale.ROOT).endsWith(".xls")) { + response.setContentType("application/vnd.ms-excel"); + } + in.transferTo(response.getOutputStream()); + } + } @At("") @Ok("beetl:/platform/zhgh/staffbenefit/therapyRecuperation/basicManage/line.html") diff --git a/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/basicManage/TheRapyRecuperationTravelAgencyController.java b/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/basicManage/TheRapyRecuperationTravelAgencyController.java index 401a291..f3d9501 100644 --- a/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/basicManage/TheRapyRecuperationTravelAgencyController.java +++ b/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/controller/basicManage/TheRapyRecuperationTravelAgencyController.java @@ -1,10 +1,11 @@ package com.budwk.app.zhgh.staffbenefit.therapyRecuperation.controller.basicManage; -import cn.afterturn.easypoi.excel.ExcelImportUtil; -import cn.afterturn.easypoi.excel.entity.ImportParams; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.StrUtil; import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.SysOfficeTemplateUtil; +import com.budwk.app.sys.models.Sys_office_template; +import com.budwk.app.zhgh.staffbenefit.therapyRecuperation.support.TherapyDownload; import com.budwk.app.zhgh.staffbenefit.therapyRecuperation.support.ViReturn; import com.budwk.app.zhgh.staffbenefit.therapyRecuperation.support.PageUtil; import com.budwk.app.base.param.PageForm; @@ -14,13 +15,11 @@ import com.budwk.app.zhgh.staffbenefit.therapyRecuperation.service.TheRapyRecupe import lombok.extern.slf4j.Slf4j; import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckPermission; -import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; import org.nutz.dao.util.cri.SqlExpressionGroup; -import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.lang.Lang; @@ -32,7 +31,20 @@ import org.nutz.mvc.upload.UploadAdaptor; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import org.nutz.trans.Trans; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.CellValue; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import java.math.BigDecimal; +import java.io.IOException; +import java.io.InputStream; +import javax.servlet.http.HttpServletResponse; /** @@ -51,6 +63,8 @@ public class TheRapyRecuperationTravelAgencyController { private TheRapyRecuperationTravelAgencyService travelAgencyService; @Inject private Dao dao; + @Inject + private SysOfficeTemplateUtil sysOfficeTemplateUtil; @At("") @Ok("beetl:/platform/zhgh/staffbenefit/therapyRecuperation/basicManage/travelAgency.html") @@ -58,6 +72,21 @@ public class TheRapyRecuperationTravelAgencyController { public void index() { } + @At + @Ok("void") + @SaCheckPermission("inclusiveTherapyV3.travelAgency") + public void downloadTemplate(HttpServletResponse response) throws IOException { + String templateCode = "inclusive_therapy_v3_travel_agency_import"; + try (InputStream in = sysOfficeTemplateUtil.getTemplate(templateCode)) { + Sys_office_template template = dao.fetch(Sys_office_template.class, Cnd.where("templateCode", "=", templateCode)); + TherapyDownload.excelResponse(response, template.getTemplateName()); + if (template.getTemplateName().toLowerCase(java.util.Locale.ROOT).endsWith(".xls")) { + response.setContentType("application/vnd.ms-excel"); + } + in.transferTo(response.getOutputStream()); + } + } + /** * 页面数据 * @@ -211,9 +240,8 @@ public class TheRapyRecuperationTravelAgencyController { */ @At @ViReturn - @SaCheckLogin + @SaCheckPermission("inclusiveTherapyV3.travelAgency") @AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"}) - @Aop(TransAop.READ_COMMITTED) public Object travelAgencyImport(TempFile file) { if (Lang.isEmpty(file)) { return Result.error("上传的文件不能为空"); @@ -222,58 +250,146 @@ public class TheRapyRecuperationTravelAgencyController { if (!List.of("xls", "xlsx").contains(extName.toLowerCase())) { return Result.error("请上传xls,xlsx文件"); } - List excelList; + List excelList = new ArrayList<>(); + List excelRowNumbers = new ArrayList<>(); - try { - ImportParams importParams = new ImportParams(); -// importParams.setTitleRows(1); - excelList = ExcelImportUtil.importExcel(file.getFile(), TheRapyTravelAgencyExcelMode.class, importParams); + try (Workbook workbook = WorkbookFactory.create(file.getFile())) { + Sheet sheet = workbook.getSheetAt(0); + Row header = sheet.getRow(0); + if (header == null) return Result.error("第一行必须是模板表头"); + DataFormatter formatter = new DataFormatter(); + FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); + Map headers = new java.util.LinkedHashMap<>(); + for (Cell cell : header) { + String name = formatter.formatCellValue(cell).trim().replace("(可填)", "").replace("(可填)", ""); + if ("编号".equals(name)) name = "排序编号"; + if ("是否启用".equals(name)) name = "是否启用(是/否)"; + if (StrUtil.isBlank(name)) continue; + if (headers.putIfAbsent(name, cell.getColumnIndex()) != null) return Result.error("模板表头重复:" + name); + } + for (String name : List.of("年度", "排序编号", "旅行社名称", "是否自由组团(是/否)", "旅行社联系人", "联系电话")) { + if (!headers.containsKey(name)) return Result.error("模板缺少必填列:" + name); + } + for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) { + Row dataRow = sheet.getRow(rowIndex); + if (dataRow == null) continue; + Map values = new java.util.HashMap<>(); + for (Map.Entry column : headers.entrySet()) { + Cell cell = dataRow.getCell(column.getValue()); + String value = formatter.formatCellValue(cell, evaluator).trim(); + if (cell != null && ("年度".equals(column.getKey()) || "最多承担人数".equals(column.getKey()))) { + CellValue evaluated = evaluator.evaluate(cell); + if (evaluated != null && evaluated.getCellType() == CellType.NUMERIC) { + value = BigDecimal.valueOf(evaluated.getNumberValue()).toPlainString(); + } + } + values.put(column.getKey(), value); + } + if (values.values().stream().allMatch(StrUtil::isBlank)) continue; + TheRapyTravelAgencyExcelMode travel = new TheRapyTravelAgencyExcelMode(); + try { + if (StrUtil.isNotBlank(values.get("年度"))) { + travel.setYear(new BigDecimal(values.get("年度")).intValueExact()); + } + } catch (NumberFormatException | ArithmeticException e) { + return Result.error("第" + (rowIndex + 1) + "行:年度须填写4位整数"); + } + try { + if (StrUtil.isNotBlank(values.get("最多承担人数"))) { + travel.setMaxUndertakeNumber(new BigDecimal(values.get("最多承担人数")).intValueExact()); + } + } catch (NumberFormatException | ArithmeticException e) { + return Result.error("第" + (rowIndex + 1) + "行:最多承担人数须填写有效整数"); + } + travel.setSerialNumber(values.get("排序编号")); + travel.setTravelAgencyName(values.get("旅行社名称")); + travel.setSignUpTravelAgency(values.get("是否自由组团(是/否)")); + travel.setContact(values.get("旅行社联系人")); + travel.setContactMobileNumber(values.get("联系电话")); + travel.setEmail(values.get("邮箱")); + travel.setOfficialWebsite(values.get("官网")); + travel.setNote(values.get("备注")); + travel.setIsDisabled(values.get("是否启用(是/否)")); + excelList.add(travel); + excelRowNumbers.add(rowIndex + 1); + } } catch (Exception e) { - log.error(e.getMessage()); - return Result.error("读取不到数据,请检查excel文件格式"); + log.error("读取旅行社导入文件失败", e); + return Result.error("读取失败,请使用新模板;年度和最多承担人数须填写整数"); } if (Lang.isEmpty(excelList)) { return Result.error("读取不到数据,请检查excel文件格式"); } + List list = new ArrayList<>(); + Map importedNumbers = new java.util.HashMap<>(); + for (int index = 0; index < excelList.size(); index++) { + TheRapyTravelAgencyExcelMode travel = excelList.get(index); + String row = "第" + excelRowNumbers.get(index) + "行:"; + travel.setSerialNumber(StrUtil.trim(travel.getSerialNumber())); + travel.setTravelAgencyName(StrUtil.trim(travel.getTravelAgencyName())); + travel.setContact(StrUtil.trim(travel.getContact())); + travel.setContactMobileNumber(StrUtil.trim(travel.getContactMobileNumber())); + travel.setSignUpTravelAgency(StrUtil.trim(travel.getSignUpTravelAgency())); + travel.setIsDisabled(StrUtil.trim(travel.getIsDisabled())); + travel.setEmail(StrUtil.trim(travel.getEmail())); + travel.setOfficialWebsite(StrUtil.trim(travel.getOfficialWebsite())); + if (travel.getYear() == null || travel.getYear() < 1000 || travel.getYear() > 9999) { + return Result.error(row + "年度须填写4位整数"); + } + if (StrUtil.isBlank(travel.getSerialNumber()) || StrUtil.isBlank(travel.getTravelAgencyName()) + || StrUtil.isBlank(travel.getContact()) || StrUtil.isBlank(travel.getContactMobileNumber())) { + return Result.error(row + "编号、旅行社名称、旅行社联系人和联系电话不能为空"); + } + if (!List.of("是", "否").contains(travel.getSignUpTravelAgency() == null ? "" : travel.getSignUpTravelAgency())) { + return Result.error(row + "是否自由组团须填写是或否"); + } + if (StrUtil.isBlank(travel.getIsDisabled())) travel.setIsDisabled("是"); + if (!List.of("是", "否").contains(travel.getIsDisabled())) return Result.error(row + "是否启用须填写是或否"); + if (!travel.getContactMobileNumber().matches("^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\\d{8}$")) { + return Result.error(row + "联系电话格式不正确"); + } + if (travel.getSerialNumber().length() > 50 || travel.getTravelAgencyName().length() > 50 + || travel.getContact().length() > 10 || StrUtil.length(travel.getEmail()) > 50 + || StrUtil.length(travel.getOfficialWebsite()) > 100 || StrUtil.length(travel.getNote()) > 100) { + return Result.error(row + "字段长度超限:编号/名称/邮箱最多50字,联系人10字,官网/备注100字"); + } + if (StrUtil.isNotBlank(travel.getEmail()) && !cn.hutool.core.lang.Validator.isEmail(travel.getEmail())) { + return Result.error(row + "邮箱格式不正确"); + } + if (StrUtil.isNotBlank(travel.getOfficialWebsite()) && !cn.hutool.core.lang.Validator.isUrl(travel.getOfficialWebsite())) { + return Result.error(row + "官网须填写完整网址"); + } + if (travel.getMaxUndertakeNumber() != null && travel.getMaxUndertakeNumber() < 0) { + return Result.error(row + "最多承担人数不能小于0"); + } + Integer previousRow = importedNumbers.putIfAbsent(travel.getSerialNumber(), excelRowNumbers.get(index)); + if (previousRow != null) return Result.error(row + "编号与第" + previousRow + "行重复"); + List existing = dao.query(TheRapyRecuperationTravelAgency.class, + Cnd.where("serialNumber", "=", travel.getSerialNumber())); + if (existing.size() > 1) return Result.error(row + "数据库中该编号对应多条旅行社记录"); + TheRapyRecuperationTravelAgency agency = existing.isEmpty() ? new TheRapyRecuperationTravelAgency() : existing.get(0); + agency.setDisabled("否".equals(travel.getIsDisabled())); + agency.setSignUpTravelAgency("是".equals(travel.getSignUpTravelAgency())); + agency.setContact(travel.getContact()); + agency.setTravelAgencyName(travel.getTravelAgencyName()); + agency.setSerialNumber(travel.getSerialNumber()); + agency.setEmail(travel.getEmail()); + agency.setMaxUndertakeNumber(travel.getMaxUndertakeNumber()); + agency.setContactMobileNumber(travel.getContactMobileNumber()); + agency.setNote(travel.getNote()); + agency.setYear(travel.getYear()); + agency.setOfficialWebsite(travel.getOfficialWebsite()); + list.add(agency); + } try { - if (excelList.stream().anyMatch(v -> StrUtil.isBlank(v.getYear().toString())) || excelList.stream().anyMatch(v -> StrUtil.isBlank(v.getTravelAgencyName()))) { - return Result.error("年度和旅行社名称不能为空"); - } - List travelAgency = ExcelImportUtil.importExcel(file.getFile(), TheRapyTravelAgencyExcelMode.class, new ImportParams()); - - List travelAgencyList = dao.query(TheRapyRecuperationTravelAgency.class, Cnd.NEW().desc("id")); - Map map = travelAgencyList.stream().collect(Collectors.toMap(v -> v.getTravelAgencyName(), v -> v.getId())); - - List list = new ArrayList<>(); - for (TheRapyTravelAgencyExcelMode travel : travelAgency) { - TheRapyRecuperationTravelAgency agency = new TheRapyRecuperationTravelAgency(); - if (Strings.isNotBlank(map.get(travel.getTravelAgencyName()))){ - agency.setId(map.get(travel.getTravelAgencyName())); - } - if (travel.getIsDisabled().equals("是")) { - agency.setDisabled(false); - } else if (travel.getIsDisabled().equals("否")) { - agency.setDisabled(true); - } - agency.setContact(travel.getContact()); - agency.setTravelAgencyName(travel.getTravelAgencyName()); - agency.setSerialNumber(travel.getSerialNumber()); - agency.setEmail(travel.getEmail()); - agency.setMaxUndertakeNumber(travel.getMaxUndertakeNumber()); - agency.setContactMobileNumber(travel.getContactMobileNumber()); - agency.setNote(travel.getNote()); - agency.setYear(travel.getYear()); - agency.setOfficialWebsite(travel.getOfficialWebsite()); - list.add(agency); - } - dao.insertOrUpdate(list); + Trans.exec(() -> dao.insertOrUpdate(list)); } catch (Exception e) { - log.error(e.getMessage()); + log.error("导入旅行社失败", e); return Result.error("导入旅行社失败"); } - return null; + return Result.success("成功导入" + list.size() + "条旅行社"); } } diff --git a/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/mode/TheRapyTravelAgencyExcelMode.java b/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/mode/TheRapyTravelAgencyExcelMode.java index 4d9a542..cfd15b3 100644 --- a/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/mode/TheRapyTravelAgencyExcelMode.java +++ b/src/main/java/com/budwk/app/zhgh/staffbenefit/therapyRecuperation/mode/TheRapyTravelAgencyExcelMode.java @@ -16,6 +16,9 @@ public class TheRapyTravelAgencyExcelMode { @Excel(name = "旅行社名称") private String travelAgencyName; + @Excel(name = "是否自由组团(是/否)") + private String signUpTravelAgency; + @Excel(name = "旅行社联系人") private String contact; diff --git a/src/main/resources/static/assets/staffbenefit/therapyRecuperation/component.js b/src/main/resources/static/assets/staffbenefit/therapyRecuperation/component.js index d3dc359..9bd09d7 100644 --- a/src/main/resources/static/assets/staffbenefit/therapyRecuperation/component.js +++ b/src/main/resources/static/assets/staffbenefit/therapyRecuperation/component.js @@ -109,7 +109,7 @@ Vue.component('table-tool', {
- {{ + {{ field.label }} @@ -126,4 +126,4 @@ Vue.component('table-tool', {
`, -}) \ No newline at end of file +}) diff --git a/src/main/resources/views/platform/zhgh/staffbenefit/therapyRecuperation/basicManage/line.html b/src/main/resources/views/platform/zhgh/staffbenefit/therapyRecuperation/basicManage/line.html index 0993076..2e7a5d6 100644 --- a/src/main/resources/views/platform/zhgh/staffbenefit/therapyRecuperation/basicManage/line.html +++ b/src/main/resources/views/platform/zhgh/staffbenefit/therapyRecuperation/basicManage/line.html @@ -153,9 +153,11 @@ layout("/platform/zhgh/staffbenefit/therapyRecuperation/layouts/platform.html"){ header-align="center" show-overflow-tooltip v-for="column in tableColumns" + :key="column.prop" > -