This commit is contained in:
2026-07-14 18:05:53 +08:00
parent cbc0dcf67d
commit fb25ad45a8
17 changed files with 660 additions and 45 deletions
@@ -7,6 +7,7 @@ import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalExportService;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalWriteService;
import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
@@ -32,6 +33,8 @@ public class ProposalExportComprehensiveController {
private ProposalExportService proposalExportService;
@Inject
private ProposalCommonService proposalCommonService;
@Inject
private ProposalWriteService proposalWriteService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/export/comprehensive/index.html")
@@ -83,6 +86,19 @@ public class ProposalExportComprehensiveController {
return Result.success(pagination);
}
/**
* 查询综合导出页面当前教代会届次配置的提案类别。
*
* @param sessionId 教代会届次ID,用于读取该届次配置的提案类别
* @return VO 列表,每项包含筛选使用的 id 和页面展示使用的 name
*/
@At
@SaCheckPermission("proposal.export.comprehensive")
@ApiOperation("查询当前届次提案类别")
public Result listSessionProposalTypes(@Valid String sessionId) {
return Result.success(proposalWriteService.listSessionProposalTypes(sessionId));
}
@At
@Ok("void")
@@ -92,6 +108,21 @@ public class ProposalExportComprehensiveController {
proposalExportService.exportSummaryAsExcel(pageForm, response);
}
/**
* 按办理单位导出提案承办单位表压缩包。
*
* @param pageForm 综合导出页面查询参数,其中 sessionId 为必传的教代会届次ID
* @param response HTTP 响应,返回内容为包含多个 XSSF Excel 的 ZIP 文件
*/
@At
@Ok("void")
@ApiOperation("按办理单位导出提案承办单位表压缩包")
@SaCheckPermission("proposal.query.comprehensive")
public void exportUndertakeUnitTablesAsZip(@Valid @Param("pageForm") ProposalQueryComprehensiveParam pageForm,
HttpServletResponse response) {
proposalExportService.exportUndertakeUnitTablesAsZip(pageForm, response);
}
@At
@Ok("void")
@@ -76,6 +76,19 @@ public class ProposalWriteController {
}
/**
* 查询当前教代会届次可用于撰写提案的类型。
*
* @param sessionId 教代会届次ID
* @return VO 列表,每项包含保存使用的 id 和页面展示使用的 name
*/
@At
@SaCheckPermission("proposal.write")
@ApiOperation("查询届次可用提案类型")
public Result listSessionProposalTypes(@Valid String sessionId) {
return Result.success(proposalWriteService.listSessionProposalTypes(sessionId));
}
@At
@SaCheckPermission("proposal.write")
@@ -84,6 +97,7 @@ public class ProposalWriteController {
@SLog(tag = "提案管理系统-我的提案", msg = "保存提案")
public Result save(@Param("info") ProposalInfo proposalInfo) {
proposalWriteService.checkCurrentUserDelegate(proposalInfo.getSessionId());
proposalWriteService.validateProposalType(proposalInfo.getSessionId(), proposalInfo.getTypeId());
if (StrUtil.isBlank(proposalInfo.getCode())) {
proposalInfo.setCode(proposalWriteService.generateProposalCode(proposalInfo.getSessionId(), proposalInfo.getDelegationId()));
}
@@ -100,6 +114,7 @@ public class ProposalWriteController {
@SLog(tag = "提案管理系统-我的提案", msg = "提交提案")
public Result submit(@Param("info") ProposalInfo proposalInfo) {
proposalWriteService.checkCurrentUserDelegate(proposalInfo.getSessionId());
proposalWriteService.validateProposalType(proposalInfo.getSessionId(), proposalInfo.getTypeId());
if (StrUtil.isBlank(proposalInfo.getCode())) {
proposalInfo.setCode(proposalWriteService.generateProposalCode(proposalInfo.getSessionId(), proposalInfo.getDelegationId()));
}
@@ -130,6 +145,7 @@ public class ProposalWriteController {
@ApiOperation("重新提交提案")
@SLog(tag = "提案管理系统-我的提案", msg = "重新提交提案")
public Result submitAgain(@Param("info") ProposalInfo proposalInfo, @Param("taskId") Long taskId) {
proposalWriteService.validateProposalType(proposalInfo.getSessionId(), proposalInfo.getTypeId());
dao.insertOrUpdate(proposalInfo);
Dict dict = Dict.create();
@@ -0,0 +1,21 @@
package com.budwk.app.zhgh.democratic.proposal.controller.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 撰写提案时当前教代会届次允许选择的提案类型。
*/
@Data
@AllArgsConstructor
@ApiModel("届次可用提案类型")
public class ProposalWriteTypeVO {
@ApiModelProperty("提案类型ID,保存提案时写入 proposal_info.typeId")
private Integer id;
@ApiModelProperty("提案类型名称,来源于当前教代会届次的提案类型配置")
private String name;
}
@@ -25,7 +25,7 @@ public class ProposalType extends BaseModel {
@Column
@Comment("名称")
@ColDefine(type = ColType.VARCHAR, width = 10)
@ColDefine(type = ColType.VARCHAR, width = 50)
private String name;
@Column
@@ -34,6 +34,14 @@ public interface ProposalExportService extends BaseService<ProposalInfo> {
*/
void exportSummaryAsExcel(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response);
/**
* 按办理单位导出提案承办单位表压缩包。
*
* @param pageForm 综合导出页面查询参数;sessionId 应传当前教代会届次ID,其他条件沿用页面查询条件
* @param response HTTP 响应,方法直接输出 ZIP;ZIP 中每个有提案的单位对应一个 XSSF Excel
*/
void exportUndertakeUnitTablesAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response);
/**
* 导出提案立案汇总表excel
*
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.democratic.proposal.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.zhgh.democratic.proposal.controller.vo.ProposalWriteTypeVO;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import javax.validation.Valid;
@@ -10,6 +11,22 @@ import java.util.Map;
public interface ProposalWriteService extends BaseService<ProposalInfo> {
/**
* 查询指定教代会届次允许选择的提案类型。
*
* @param sessionId 教代会届次ID
* @return 提案类型 VO 列表,id 用于保存,name 用于页面展示
*/
List<ProposalWriteTypeVO> listSessionProposalTypes(String sessionId);
/**
* 校验所选提案类型是否属于当前教代会届次。
*
* @param sessionId 教代会届次ID
* @param typeId 提案类型ID
*/
void validateProposalType(String sessionId, Integer typeId);
List<Sys_dict> listSource(String sessionId);
/**
@@ -158,7 +158,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl<ProposalInfo> imp
pages.put("preAudit", standardOrderColumns());
pages.put("committeeFiling", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "caseFilingResult", "info.caseFilingResult", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("caseCheck", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state", "finishTime", "t.finishTime"));
pages.put("exportComprehensive", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "caseFilingType", "info.caseFilingType", "merge", "merge", "brief", "info.brief", "measures", "info.measures", "undertakeUnits", "masterUnitName", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("exportComprehensive", orderColumns("code", "info.code", "caseFilingCode", "info.caseFilingCode", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "caseFilingType", "info.caseFilingType", "merge", "merge", "brief", "info.brief", "measures", "info.measures", "undertakeUnits", "masterUnitName", "curTaskName", "curTaskName", "instanceState", "ins.state"));
pages.put("exportSingleCustom", standardOrderColumns());
pages.put("configUnit", orderColumns("name", "t1.name", "code", "t1.code", "unitLeader", "u2.username"));
pages.put("configType", orderColumns("name", "name", "code", "code"));
@@ -4,8 +4,10 @@ import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.param.ExportTableColumns;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
@@ -27,7 +29,15 @@ import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xwpf.usermodel.BreakType;
import org.apache.poi.xwpf.usermodel.IBodyElement;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
@@ -51,6 +61,8 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -61,6 +73,9 @@ import java.util.zip.ZipOutputStream;
@Slf4j
public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> implements ProposalExportService {
private static final String WORK_SUGGESTION_SECTION_MARKER = "__WORK_SUGGESTION_SECTION__";
private static final int UNDERTAKE_UNIT_EXPORT_COLUMN_COUNT = 8;
@Inject
private ProposalCommonService proposalCommonService;
@Inject
@@ -246,6 +261,239 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
CommonDownloadUtil.download(title + ".xlsx", workbook, response);
}
/**
* 按办理单位生成提案承办单位表。查询时忽略页面单个承办单位条件,其他查询条件继续生效;
* 同一提案涉及多个单位时,会进入每个相关单位的 Excel,并保留完整主办、协办单位信息。
*
* @param pageForm 综合导出页面查询参数,sessionId 必须为当前教代会届次ID
* @param response HTTP 响应,直接输出包含多个 XSSF Excel 的 ZIP 文件
*/
@Override
public void exportUndertakeUnitTablesAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) {
if (pageForm == null || StrUtil.isBlank(pageForm.getSessionId())) {
throw new BaseException("请选择教代会");
}
Teacher_congress_session session = dao().fetch(Teacher_congress_session.class, pageForm.getSessionId());
if (session == null) {
throw new BaseException("教代会届次不存在");
}
List<NutMap> unitRows = queryUndertakeUnitExportRows(pageForm);
if (Lang.isEmpty(unitRows)) {
throw new BaseException("暂无可导出的承办单位提案数据");
}
Map<String, NutMap> proposalRows = new LinkedHashMap<>();
Map<String, String> unitNames = new LinkedHashMap<>();
Map<String, LinkedHashSet<String>> unitProposalIds = new LinkedHashMap<>();
for (NutMap row : unitRows) {
String proposalId = row.getString("id");
String unitId = row.getString("unitId");
if (StrUtil.isBlank(proposalId) || StrUtil.isBlank(unitId)) {
continue;
}
String unitName = StrUtil.blankToDefault(row.getString("unitName"), "未命名单位");
NutMap proposalRow = proposalRows.computeIfAbsent(proposalId, key -> NutMap.NEW()
.addv("id", proposalId)
.addv("code", row.getString("code"))
.addv("caseFilingCode", row.getString("caseFilingCode"))
.addv("name", row.getString("name"))
.addv("createUserName", row.getString("createUserName"))
.addv("caseFilingResult", row.getString("caseFilingResult"))
.addv("masterUnitNames", new LinkedHashSet<String>())
.addv("slaveUnitNames", new LinkedHashSet<String>()));
getUnitNameSet(proposalRow, Boolean.TRUE.equals(row.getBoolean("isMaster"))
? "masterUnitNames" : "slaveUnitNames").add(unitName);
unitNames.putIfAbsent(unitId, unitName);
unitProposalIds.computeIfAbsent(unitId, key -> new LinkedHashSet<>()).add(proposalId);
}
if (unitProposalIds.isEmpty()) {
throw new BaseException("暂无可导出的承办单位提案数据");
}
String title = StrUtil.format("{}第{}教职工代表大会第{}会议提案承办单位表",
Globals.AppName, session.getJ(), session.getC());
ByteArrayOutputStream zipBytes = new ByteArrayOutputStream();
Map<String, Integer> zipEntryNameCounts = new HashMap<>();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(zipBytes)) {
for (Map.Entry<String, LinkedHashSet<String>> unitEntry : unitProposalIds.entrySet()) {
String unitName = unitNames.get(unitEntry.getKey());
List<NutMap> excelRows = buildUndertakeUnitExcelRows(unitEntry.getValue(), proposalRows);
try (Workbook workbook = buildUndertakeUnitWorkbook(title, unitName, excelRows);
ByteArrayOutputStream workbookBytes = new ByteArrayOutputStream()) {
workbook.write(workbookBytes);
String baseEntryName = "提案承办单位表-" + sanitizeZipFileName(unitName);
int sameNameCount = zipEntryNameCounts.merge(baseEntryName, 1, Integer::sum);
String entryName = baseEntryName + (sameNameCount > 1 ? "-" + sameNameCount : "") + ".xlsx";
zipOutputStream.putNextEntry(new ZipEntry(entryName));
workbookBytes.writeTo(zipOutputStream);
zipOutputStream.closeEntry();
}
}
zipOutputStream.finish();
} catch (IOException e) {
log.error("按办理单位导出提案承办单位表失败:{}", e.getMessage(), e);
throw new BaseException("导出提案承办单位表失败");
}
CommonDownloadUtil.download(sanitizeZipFileName(title) + ".zip", zipBytes.toByteArray(), response);
}
/**
* 查询当前届次已确定立案或作为工作建议的提案及其全部办理单位。
* 页面承办单位单选条件会被清空,避免只生成一个单位文件;其他页面条件继续沿用。
*/
private List<NutMap> queryUndertakeUnitExportRows(ProposalQueryComprehensiveParam pageForm) {
ProposalQueryComprehensiveParam exportPageForm = new ProposalQueryComprehensiveParam();
BeanUtil.copyProperties(pageForm, exportPageForm);
exportPageForm.setUnderTakeUnitId(null);
Sql sql = Sqls.create("""
SELECT DISTINCT
info.id,
info.code,
info.caseFilingCode,
info.name,
info.createUserName,
info.caseFilingResult,
pru.unitId,
pru.unitName,
pru.isMaster
FROM
proposal_info info
LEFT JOIN proposal_type type ON type.id = info.typeId
LEFT JOIN teacher_congress_session tcs ON tcs.id = info.sessionId
LEFT JOIN teacher_congress_delegation tcd ON tcd.id = info.delegationId
LEFT JOIN teacher_congress_delegate tcde ON tcde.loginName = info.createUserLoginName
INNER JOIN proposal_reply_unit pru ON pru.proposalId = info.id
$condition
""");
Cnd cnd = Cnd.where("info.sessionId", "=", pageForm.getSessionId());
cnd.and("info.caseFilingResult", "in", List.of("CONFIRM_FILING", "SUGGESTION"));
ProposalQueryComprehensiveParam.buildSearch(cnd, exportPageForm);
cnd.asc("info.code");
cnd.asc("pru.unitName");
sql.setCondition(cnd);
return listMap(sql);
}
/**
* 将某个办理单位涉及的提案整理为 Excel 行。确定立案在前,工作建议通过标记行单独分组。
*/
private List<NutMap> buildUndertakeUnitExcelRows(LinkedHashSet<String> proposalIds,
Map<String, NutMap> proposalRows) {
List<NutMap> filingRows = new ArrayList<>();
List<NutMap> suggestionRows = new ArrayList<>();
for (String proposalId : proposalIds) {
NutMap proposalRow = proposalRows.get(proposalId);
if (proposalRow == null) {
continue;
}
LinkedHashSet<String> masterUnitNames = getUnitNameSet(proposalRow, "masterUnitNames");
LinkedHashSet<String> slaveUnitNames = getUnitNameSet(proposalRow, "slaveUnitNames");
NutMap excelRow = NutMap.NEW()
.addv("code", proposalRow.getString("code"))
.addv("caseFilingCode", proposalRow.getString("caseFilingCode"))
.addv("name", proposalRow.getString("name"))
.addv("createUserName", proposalRow.getString("createUserName"))
.addv("masterUnitNames", "")
.addv("slaveUnitNames", "")
.addv("workSuggestionUnitNames", "")
.addv("remark", "");
if ("CONFIRM_FILING".equals(proposalRow.getString("caseFilingResult"))) {
excelRow.put("masterUnitNames", String.join("", masterUnitNames));
excelRow.put("slaveUnitNames", String.join("", slaveUnitNames));
filingRows.add(excelRow);
} else {
LinkedHashSet<String> workSuggestionUnitNames = new LinkedHashSet<>(masterUnitNames);
workSuggestionUnitNames.addAll(slaveUnitNames);
excelRow.put("workSuggestionUnitNames", String.join("", workSuggestionUnitNames));
suggestionRows.add(excelRow);
}
}
if (!suggestionRows.isEmpty()) {
filingRows.add(NutMap.NEW()
.addv("code", WORK_SUGGESTION_SECTION_MARKER)
.addv("caseFilingCode", "")
.addv("name", "")
.addv("createUserName", "")
.addv("masterUnitNames", "")
.addv("slaveUnitNames", "")
.addv("workSuggestionUnitNames", "")
.addv("remark", ""));
filingRows.addAll(suggestionRows);
}
return filingRows;
}
/**
* 创建单个办理单位的 XSSF Excel,并把工作建议标记行合并为分组标题。
*/
private Workbook buildUndertakeUnitWorkbook(String title, String unitName, List<NutMap> excelRows) {
List<ExcelExportEntity> exportEntities = new ArrayList<>();
exportEntities.add(new ExcelExportEntity("提案编号", "code", 18));
exportEntities.add(new ExcelExportEntity("立案编号", "caseFilingCode", 18));
exportEntities.add(new ExcelExportEntity("提案名称", "name", 45));
exportEntities.add(new ExcelExportEntity("提案人", "createUserName", 15));
exportEntities.add(new ExcelExportEntity("主办单位", "masterUnitNames", 28));
exportEntities.add(new ExcelExportEntity("协办单位", "slaveUnitNames", 28));
exportEntities.add(new ExcelExportEntity("工作建议送达单位", "workSuggestionUnitNames", 32));
exportEntities.add(new ExcelExportEntity("备注", "remark", 20));
for (ExcelExportEntity exportEntity : exportEntities) {
exportEntity.setWrap(true);
}
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
exportParams.setTitle(title);
exportParams.setSecondTitle("" + unitName + "");
exportParams.setSheetName("提案承办单位表");
exportParams.setHeaderHeight(30);
exportParams.setHeight((short) 24);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, excelRows);
mergeWorkSuggestionSection(workbook);
return workbook;
}
/**
* 查找工作建议标记行,将八个单元格合并并设置为居中的分组标题。
*/
private void mergeWorkSuggestionSection(Workbook workbook) {
Sheet sheet = workbook.getSheetAt(0);
for (Row row : sheet) {
Cell firstCell = row.getCell(0);
if (firstCell == null || !WORK_SUGGESTION_SECTION_MARKER.equals(firstCell.toString())) {
continue;
}
CellStyle sectionStyle = workbook.createCellStyle();
sectionStyle.cloneStyleFrom(firstCell.getCellStyle());
sectionStyle.setAlignment(HorizontalAlignment.CENTER);
sectionStyle.setVerticalAlignment(VerticalAlignment.CENTER);
Font sectionFont = workbook.createFont();
sectionFont.setFontName("宋体");
sectionFont.setFontHeightInPoints((short) 11);
sectionFont.setBold(true);
sectionStyle.setFont(sectionFont);
for (int columnIndex = 0; columnIndex < UNDERTAKE_UNIT_EXPORT_COLUMN_COUNT; columnIndex++) {
Cell cell = row.getCell(columnIndex);
if (cell == null) {
cell = row.createCell(columnIndex);
}
cell.setCellValue(columnIndex == 0 ? "作为工作建议的提案" : "");
cell.setCellStyle(sectionStyle);
}
row.setHeightInPoints(24);
sheet.addMergedRegion(new CellRangeAddress(row.getRowNum(), row.getRowNum(), 0,
UNDERTAKE_UNIT_EXPORT_COLUMN_COUNT - 1));
return;
}
}
/** 获取提案行中用于去重且保持顺序的办理单位名称集合。 */
@SuppressWarnings("unchecked")
private LinkedHashSet<String> getUnitNameSet(NutMap proposalRow, String key) {
return (LinkedHashSet<String>) proposalRow.get(key);
}
@Override
public void exportProposalRegisterSummaryAsExcel(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) {
Sql sql = exportComprehensiveSql(pageForm);
@@ -4,6 +4,7 @@ import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.models.Sys_user;
@@ -11,6 +12,7 @@ import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.constants.ProposalState;
import com.budwk.app.zhgh.democratic.proposal.controller.vo.ProposalWriteTypeVO;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalType;
import com.budwk.app.zhgh.democratic.proposal.service.ProposalWriteService;
@@ -24,6 +26,7 @@ import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Strings;
import java.util.ArrayList;
@@ -44,6 +47,70 @@ public class ProposalWriteServiceImpl extends BaseServiceImpl<ProposalInfo> impl
super(dao);
}
@Override
public List<ProposalWriteTypeVO> listSessionProposalTypes(String sessionId) {
List<String> typeNames = getSessionProposalTypeNames(sessionId);
if (typeNames.isEmpty()) {
return List.of();
}
List<ProposalType> proposalTypes = dao().query(ProposalType.class,
Cnd.where(ProposalType::getName, "in", typeNames));
List<ProposalWriteTypeVO> result = new ArrayList<>();
for (String typeName : typeNames) {
ProposalType proposalType = proposalTypes.stream()
.filter(item -> typeName.equals(item.getName()))
.findFirst()
.orElse(null);
if (proposalType != null) {
result.add(new ProposalWriteTypeVO(proposalType.getId(), proposalType.getName()));
}
}
return result;
}
@Override
public void validateProposalType(String sessionId, Integer typeId) {
if (typeId == null) {
throw new BaseException("请选择提案类型");
}
ProposalType proposalType = dao().fetch(ProposalType.class, typeId);
if (proposalType == null || !getSessionProposalTypeNames(sessionId).contains(proposalType.getName())) {
throw new BaseException("所选提案类型不属于当前教代会届次");
}
}
/**
* 解析指定届次配置的提案类型。标准数据为 JSON 数组,同时兼容历史单值字符串。
*
* @param sessionId 教代会届次ID
* @return 按届次配置顺序排列的提案类型名称;未配置时返回空列表
*/
private List<String> getSessionProposalTypeNames(String sessionId) {
if (StrUtil.isBlank(sessionId)) {
throw new BaseException("所属教代会不能为空");
}
Teacher_congress_session session = dao().fetch(Teacher_congress_session.class, sessionId);
if (session == null) {
throw new BaseException("所属教代会不存在");
}
if (StrUtil.isBlank(session.getProposalType())) {
return List.of();
}
String proposalTypeValue = StrUtil.trim(session.getProposalType());
if (!proposalTypeValue.startsWith("[")) {
return List.of(proposalTypeValue);
}
try {
return Json.fromJsonAsList(String.class, proposalTypeValue).stream()
.map(StrUtil::trim)
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
} catch (Exception e) {
throw new BaseException("教代会届次提案类型配置格式不正确");
}
}
@Override
public List<Sys_dict> listSource(String sessionId) {
Sql sql = Sqls.create("""
@@ -224,6 +291,7 @@ public class ProposalWriteServiceImpl extends BaseServiceImpl<ProposalInfo> impl
throw new RuntimeException("没有开启的教代会");
}
Teacher_congress_session teacherCongressSession = teacherCongressSessions.get(0);
validateProposalType(teacherCongressSession.getId(), proposalTypeObj.getId());
// 保存到数据库
ProposalInfo proposalInfo = new ProposalInfo();
@@ -1,7 +1,6 @@
package com.budwk.app.zhgh.democratic.teachercongress.prepare.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
@@ -87,16 +86,7 @@ public class TeacherCongressSessionController {
@SaCheckPermission("tc.prepare.session")
@ApiOperation(value = "修改届次信息")
public Result update(@Valid Teacher_congress_session session) {
int count = dao.count(Teacher_congress_session.class,
Cnd.where(Teacher_congress_session::getJ, "=", session.getJ())
.and(Teacher_congress_session::getC, "=", session.getC())
.and(Teacher_congress_session::getId, "!=", session.getId())
);
if (count > 0) {
throw new BaseException("请勿重复创建");
}
session.setFullName("" + session.getJ() + "" + session.getC());
dao.updateIgnoreNull(session);
teacherCongressSessionService.updateSession(session);
return Result.success();
}
@@ -44,10 +44,10 @@ public class Teacher_congress_session extends BaseModel {
private String fullName;
@Column
@Comment("提案类型")
@ColDefine(type = ColType.VARCHAR, width = 50)
@Comment("提案类型JSON数组)")
@ColDefine(type = ColType.VARCHAR, width = 1000)
@NotBlank(message = "提案类型不能为空")
@Size(max = 50, message = "提案类型最多50个字")
@Size(max = 1000, message = "提案类型数据不能超过1000个字")
private String proposalType;
@Column
@@ -20,5 +20,12 @@ public interface TeacherCongressSessionService extends BaseService<Teacher_congr
void insertSession(Teacher_congress_session session, boolean isExtend);
/**
* 修改届次基础信息并统一校验多选提案类型。
*
* @param session 届次信息,其中 proposalType 为 JSON 字符串数组
*/
void updateSession(Teacher_congress_session session);
void deleteSession(String id);
}
@@ -16,6 +16,7 @@ import com.budwk.app.sys.models.Sys_user_role;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalType;
import com.budwk.app.zhgh.democratic.teachercongress.delegate.models.Teacher_congress_delegate;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation;
import com.budwk.app.zhgh.democratic.teachercongress.delegation.models.Teacher_congress_delegation_union;
@@ -36,10 +37,13 @@ import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.random.R;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -66,7 +70,17 @@ public class TeacherCongressSessionServiceImpl extends BaseServiceImpl<Teacher_c
"WHERE proposalType IS NOT NULL AND TRIM(proposalType) <> '' ORDER BY proposalType");
sql.setCallback(Sqls.callback.strList());
dao().execute(sql);
return sql.getList(String.class).stream()
LinkedHashSet<String> proposalTypes = new LinkedHashSet<>();
for (String proposalTypeValue : sql.getList(String.class)) {
for (String proposalType : parseProposalTypes(proposalTypeValue)) {
String normalizedProposalType = StrUtil.trim(proposalType);
if (StrUtil.isNotBlank(normalizedProposalType)) {
proposalTypes.add(normalizedProposalType);
}
}
}
return proposalTypes.stream()
.sorted()
.map(TeacherCongressSessionProposalTypeVO::new)
.toList();
}
@@ -74,6 +88,7 @@ public class TeacherCongressSessionServiceImpl extends BaseServiceImpl<Teacher_c
@Override
@Aop(TransAop.READ_COMMITTED)
public void insertSession(Teacher_congress_session session, boolean isExtend) {
normalizeAndValidateProposalTypes(session);
int count = count(Cnd.where(Teacher_congress_session::getJ, "=", session.getJ())
.and(Teacher_congress_session::getC, "=", session.getC()));
if (count > 0) {
@@ -257,6 +272,114 @@ public class TeacherCongressSessionServiceImpl extends BaseServiceImpl<Teacher_c
}
}
/**
* 修改届次基础信息。提案类型在保存前统一完成 JSON 解析、去重和长度校验,
* 避免新增与编辑使用不同的数据格式。
*
* @param session 届次信息,其中 proposalType 应传 JSON 字符串数组
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateSession(Teacher_congress_session session) {
normalizeAndValidateProposalTypes(session);
int count = count(Cnd.where(Teacher_congress_session::getJ, "=", session.getJ())
.and(Teacher_congress_session::getC, "=", session.getC())
.and(Teacher_congress_session::getId, "!=", session.getId()));
if (count > 0) {
throw new BaseException("请勿重复创建");
}
session.setFullName("" + session.getJ() + "" + session.getC());
dao().updateIgnoreNull(session);
}
/**
* 将多选提案类型规范化为 JSON 数组字符串。每个类型去除首尾空格、去重且最多 50 个字符,
* 整体序列化结果最多 1000 个字符。
*
* @param session 待新增或修改的届次实体
*/
private void normalizeAndValidateProposalTypes(Teacher_congress_session session) {
List<String> values = parseProposalTypes(session.getProposalType());
LinkedHashSet<String> normalizedValues = new LinkedHashSet<>();
for (String value : values) {
String normalizedValue = StrUtil.trim(value);
if (StrUtil.isBlank(normalizedValue)) {
continue;
}
if (normalizedValue.length() > 50) {
throw new BaseException("单个提案类型不能超过50个字符");
}
normalizedValues.add(normalizedValue);
}
if (normalizedValues.isEmpty()) {
throw new BaseException("请至少选择或输入一个提案类型");
}
String proposalTypeJson = Json.toJson(new ArrayList<>(normalizedValues));
if (proposalTypeJson.length() > 1000) {
throw new BaseException("提案类型数据不能超过1000个字符");
}
session.setProposalType(proposalTypeJson);
syncProposalTypes(normalizedValues);
}
/**
* 将届次中新输入的类型同步到全局提案类型表,以便继续使用 proposal_info.typeId
* 以及现有列表、统计和导出的关联查询。已有同名类型直接复用,不删除历史类型。
*
* @param proposalTypeNames 当前届次规范化、去重后的提案类型名称
*/
private void syncProposalTypes(LinkedHashSet<String> proposalTypeNames) {
ProposalType lastType = dao().fetch(ProposalType.class, Cnd.NEW().desc(ProposalType::getSort));
int nextSort = lastType == null || lastType.getSort() == null ? 1 : lastType.getSort() + 1;
for (String proposalTypeName : proposalTypeNames) {
ProposalType proposalType = dao().fetch(ProposalType.class,
Cnd.where(ProposalType::getName, "=", proposalTypeName));
if (proposalType != null) {
continue;
}
ProposalType newProposalType = new ProposalType();
newProposalType.setCode(generateProposalTypeCode());
newProposalType.setName(proposalTypeName);
newProposalType.setSort(nextSort++);
dao().insert(newProposalType);
}
}
/**
* 生成不超过 10 个字符的内部提案类型编码。
*
* @return 以 TC 开头且在 proposal_type 表中未使用的编码
*/
private String generateProposalTypeCode() {
String code;
do {
code = "TC" + R.UU32().substring(0, 8).toUpperCase();
} while (dao().count(ProposalType.class, Cnd.where(ProposalType::getCode, "=", code)) > 0);
return code;
}
/**
* 解析届次中保存的提案类型。标准数据为 JSON 字符串数组,同时兼容调整前可能存在的单值数据。
*
* @param proposalTypeValue 数据库字段值或前端提交的 JSON 字符串
* @return 提案类型字符串列表;空值返回空列表
*/
private List<String> parseProposalTypes(String proposalTypeValue) {
if (StrUtil.isBlank(proposalTypeValue)) {
return List.of();
}
String value = proposalTypeValue.trim();
if (!value.startsWith("[")) {
return List.of(value);
}
try {
return Json.fromJsonAsList(String.class, value);
} catch (Exception e) {
throw new BaseException("提案类型数据格式不正确");
}
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void deleteSession(String id) {