This commit is contained in:
@jyuhsin
2026-03-31 16:55:35 +08:00
parent dc71dc22d8
commit 6adf949e3e
184 changed files with 2333 additions and 556 deletions
@@ -104,6 +104,14 @@ public class ProposalExportComprehensiveController {
proposalExportService.exportProposalStatisticsAsZip(pageForm, response);
}
@At
@Ok("void")
@ApiOperation("导出全部提案为单个DOCX")
@SaCheckPermission("proposal.query.comprehensive")
public void exportAllProposalAsDocx(@Valid @Param("pageForm") ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) {
proposalExportService.exportProposalStatisticsAsDocx(pageForm, response);
}
@At
@Ok("void")
@ApiOperation("导出全部提案反馈表ZIP")
@@ -0,0 +1,458 @@
package com.budwk.app.zhgh.democratic.proposal.controller.export;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.flow.vo.ProcessTaskVO;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.ddr.poi.html.HtmlRenderPolicy;
import org.nutz.dao.Cnd;
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.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@IocBean
@At("/platform/proposal/export/singleCustom")
@Slf4j
@Ok("json:full")
@Api(tags = "提案导出-单个提案表自定义")
public class ProposalExportSingleCustomController {
@Inject
private BaseService baseService;
@Inject
private FlowEngine flowEngine;
@Inject
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/export/singleCustom/index.html")
@SaCheckPermission("proposal.query.comprehensive")
public void index() {
}
@At
@ApiOperation("分页提案列表")
@SaCheckPermission("proposal.query.comprehensive")
public Result pageData(@Valid ProposalSearchParam pageForm) {
Sql sql = Sqls.create("""
SELECT
info.*,
type.name AS typeName,
tcs.fullName AS sessionName,
tcd.name AS delegationName,
ins.id AS instanceId,
ins.state instanceState,
t.displayName curTaskName
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 wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10
$condition
""");
Cnd cnd = Cnd.NEW();
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
cnd.desc("info.createdAt");
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@ApiOperation("查询可导出环节")
@SaCheckPermission("proposal.query.comprehensive")
public Result stageOptions(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
ProcessInstance instance = baseService.dao().fetch(ProcessInstance.class, Cnd.where("businessNo", "=", id));
if (instance == null) {
return Result.success(new ArrayList<>());
}
Sql sql = Sqls.create("""
SELECT
t.taskName AS value,
MAX(t.displayName) AS label,
MIN(t.id) AS seq
FROM wf_process_task t
WHERE t.processInstanceId = @processInstanceId
AND t.taskState IN (@doing, @finished)
AND t.taskName != 'startTask'
AND IFNULL(t.displayName, '') != ''
GROUP BY t.taskName
ORDER BY seq ASC
""");
sql.params().set("processInstanceId", instance.getId());
sql.params().set("doing", ProcessTaskStateEnum.DOING.getCode());
sql.params().set("finished", ProcessTaskStateEnum.FINISHED.getCode());
return Result.success(baseService.listMap(sql));
}
@At
@Ok("void")
@ApiOperation("按选择环节导出单个提案表")
@SaCheckPermission("proposal.query.comprehensive")
public void exportSelectedDocx(String id, @Param("stageTaskNamesJson") String stageTaskNamesJson, HttpServletResponse response) {
if (StrUtil.isBlank(id)) {
throw new IllegalArgumentException("提案ID不能为空");
}
NutMap proposal = queryProposalBase(id);
if (proposal == null) {
throw new IllegalArgumentException("提案不存在");
}
List<String> selectedStageTaskNames = parseSelectedStageTaskNames(stageTaskNamesJson);
List<NutMap> selectedTasks = querySelectedTasks(id, selectedStageTaskNames);
byte[] templateBytes = buildTemplateDocxBytes(id);
byte[] finalBytes = appendSelectedStageSection(templateBytes, selectedTasks);
String fileName = proposal.getString("code", "proposal") + "-" + proposal.getString("name", "提案") + "-自定义导出.docx";
CommonDownloadUtil.download(fileName, finalBytes, response);
}
private byte[] buildTemplateDocxBytes(String id) {
NutMap baseInfo = queryProposalBase(id);
if (baseInfo == null) {
throw new IllegalArgumentException("提案不存在");
}
String brief = baseInfo.getString("brief", "");
String measures = baseInfo.getString("measures", "");
baseInfo.put("brief", sysOfficeTemplateUtil.convertRichTextToDocText(brief));
baseInfo.put("measures", sysOfficeTemplateUtil.convertRichTextToDocText(measures));
ProcessInstance processInstance = baseService.dao().fetch(ProcessInstance.class, Cnd.where("businessNo", "=", id));
List<ProcessTaskVO> doneTaskVos = new ArrayList<>();
if (processInstance != null) {
List<ProcessTask> doneTaskList = flowEngine.processTaskService().getDoneTaskList(processInstance.getId(), null);
for (ProcessTask doneTask : doneTaskList) {
ProcessTaskVO taskVO = flowEngine.processTaskService().findById(doneTask.getId());
if (taskVO != null) {
doneTaskVos.add(taskVO);
}
}
}
Map<String, List<ProcessTaskVO>> taskGroups = doneTaskVos.stream().collect(Collectors.groupingBy(ProcessTaskVO::getDisplayName));
List<NutMap> secondInfos = new ArrayList<>();
List<ProcessTaskVO> secondTaskVos = taskGroups.get("提案附议");
if (secondTaskVos != null) {
for (ProcessTaskVO secondTaskVO : secondTaskVos) {
Dict taskFormData = secondTaskVO.getTaskFormData();
if (taskFormData == null) {
continue;
}
NutMap secondInfo = new NutMap();
secondInfo.put("s_username", taskFormData.getStr("userName"));
secondInfo.put("s_unitName", taskFormData.getStr("unitName"));
String loginName = taskFormData.getStr("loginName");
if (StrUtil.isNotBlank(loginName)) {
Sql taskUserSql = Sqls.create("select mobile from sys_user where loginname=@loginname and delFlag != 1 limit 0,1");
taskUserSql.setParam("loginname", loginName);
taskUserSql.setCallback(Sqls.callback.map());
baseService.dao().execute(taskUserSql);
NutMap taskUserMap = (NutMap) taskUserSql.getResult();
secondInfo.put("s_mobile", taskUserMap == null ? "" : taskUserMap.getString("mobile", ""));
} else {
secondInfo.put("s_mobile", "");
}
secondInfos.add(secondInfo);
}
}
List<NutMap> delegationAuditList = new ArrayList<>();
List<ProcessTaskVO> delegationTaskVos = taskGroups.get("团长审核");
if (delegationTaskVos != null) {
for (ProcessTaskVO delegationTaskVO : delegationTaskVos) {
Dict taskFormData = delegationTaskVO.getTaskFormData();
if (taskFormData == null) {
continue;
}
NutMap delegationAudit = new NutMap();
delegationAudit.put("opinion", taskFormData.getStr("opinion"));
delegationAudit.put("username", taskFormData.getStr("userName"));
delegationAudit.put("auditTime", delegationTaskVO.getFinishTime() == null ? "" : DateUtil.format(delegationTaskVO.getFinishTime(), "yyyy-MM-dd HH:mm:ss"));
delegationAuditList.add(delegationAudit);
}
}
Map<String, Object> docCellDataMap = new HashMap<>();
docCellDataMap.put("proposalCode", baseInfo.getString("code", ""));
docCellDataMap.put("createTime", baseInfo.getString("createTime", ""));
docCellDataMap.put("username", baseInfo.getString("createUserName", ""));
docCellDataMap.put("delegationName", baseInfo.getString("delegationName", ""));
docCellDataMap.put("unitName", baseInfo.getString("unitName", ""));
docCellDataMap.put("mobile", baseInfo.getString("mobile", ""));
docCellDataMap.put("proposalName", baseInfo.getString("name", ""));
docCellDataMap.put("brief", baseInfo.getString("brief", ""));
docCellDataMap.put("measures", baseInfo.getString("measures", ""));
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
HtmlRenderPolicy htmlRenderPolicy = new HtmlRenderPolicy();
htmlRenderPolicy.getConfig().setShowDefaultTableBorderInTableCell(true);
Configure config = Configure.builder()
.bind("secondedList", policy)
.bind("seconders", policy)
.bind("brief", htmlRenderPolicy)
.bind("measures", htmlRenderPolicy)
.bind("taskFormData.tf_opinion", htmlRenderPolicy)
.build();
Map<String, Object> renderDocCellDataMap = MapUtil.of("proposal", docCellDataMap);
renderDocCellDataMap.put("secondedList", secondInfos);
renderDocCellDataMap.put("delegationAuditList", delegationAuditList);
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate("proposal"), config).render(renderDocCellDataMap).writeAndClose(out);
return out.toByteArray();
} catch (IOException e) {
log.error("提案模板导出失败, proposalId={}, error={}", id, e.getMessage());
throw new RuntimeException("导出失败", e);
}
}
private byte[] appendSelectedStageSection(byte[] templateBytes, List<NutMap> selectedTasks) {
if (templateBytes == null || templateBytes.length == 0) {
throw new RuntimeException("模板文档为空");
}
if (selectedTasks == null) {
selectedTasks = new ArrayList<>();
}
try (XWPFDocument doc = new XWPFDocument(new ByteArrayInputStream(templateBytes));
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
XWPFParagraph titleP = doc.createParagraph();
titleP.setAlignment(ParagraphAlignment.LEFT);
XWPFRun titleRun = titleP.createRun();
titleRun.setBold(true);
titleRun.setText("附加环节内容(按勾选导出)");
if (selectedTasks.isEmpty()) {
XWPFParagraph emptyP = doc.createParagraph();
emptyP.createRun().setText("未勾选流程环节,或所选环节暂无数据。");
} else {
appendStageContent(doc, selectedTasks);
}
doc.write(out);
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException("导出失败", e);
}
}
private NutMap queryProposalBase(String id) {
Sql sql = Sqls.create("""
SELECT
info.id,
info.code,
info.name,
info.brief,
info.measures,
info.createUserName,
info.createUserLoginName,
DATE_FORMAT(info.createTime, '%Y-%m-%d') AS createTime,
IFNULL(type.name, '') AS typeName,
IFNULL(tcs.fullName, '') AS sessionName,
IFNULL(tcd.name, '') AS delegationName,
IFNULL(tcde.unitName, '') AS unitName,
IFNULL(tcde.mobile, '') AS mobile
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
WHERE info.id = @id
""");
sql.setParam("id", id);
sql.setCallback(Sqls.callback.map());
baseService.dao().execute(sql);
return (NutMap) sql.getResult();
}
private List<String> parseSelectedStageTaskNames(String stageTaskNamesJson) {
if (StrUtil.isBlank(stageTaskNamesJson)) {
return new ArrayList<>();
}
try {
String[] arr = JSONUtil.toBean(stageTaskNamesJson, String[].class);
if (arr == null) {
return new ArrayList<>();
}
return Arrays.stream(arr).filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
} catch (Exception e) {
log.warn("解析环节参数失败: {}", stageTaskNamesJson);
return new ArrayList<>();
}
}
private List<NutMap> querySelectedTasks(String proposalId, List<String> selectedStageTaskNames) {
ProcessInstance instance = baseService.dao().fetch(ProcessInstance.class, Cnd.where("businessNo", "=", proposalId));
if (instance == null || selectedStageTaskNames.isEmpty()) {
return new ArrayList<>();
}
Sql sql = Sqls.create("""
SELECT
t.id,
t.taskName,
t.displayName,
t.finishTime,
t.taskState
FROM wf_process_task t
WHERE t.processInstanceId = @processInstanceId
AND t.taskState IN (@doing, @finished)
AND t.taskName IN (@taskNames)
ORDER BY t.id ASC
""");
sql.params().set("processInstanceId", instance.getId());
sql.params().set("doing", ProcessTaskStateEnum.DOING.getCode());
sql.params().set("finished", ProcessTaskStateEnum.FINISHED.getCode());
sql.params().set("taskNames", selectedStageTaskNames);
List<NutMap> rawList = baseService.listMap(sql);
Map<String, Integer> stageOrderMap = new LinkedHashMap<>();
for (int i = 0; i < selectedStageTaskNames.size(); i++) {
stageOrderMap.put(selectedStageTaskNames.get(i), i);
}
rawList.sort((a, b) -> {
int orderA = stageOrderMap.getOrDefault(a.getString("taskName", ""), Integer.MAX_VALUE);
int orderB = stageOrderMap.getOrDefault(b.getString("taskName", ""), Integer.MAX_VALUE);
if (orderA != orderB) {
return Integer.compare(orderA, orderB);
}
return Long.compare(a.getLong("id", 0L), b.getLong("id", 0L));
});
return rawList;
}
private void appendStageContent(XWPFDocument doc, List<NutMap> selectedTasks) {
Map<String, List<NutMap>> grouped = selectedTasks.stream()
.collect(Collectors.groupingBy(v -> v.getString("taskName", ""), LinkedHashMap::new, Collectors.toList()));
int stageIndex = 1;
for (Map.Entry<String, List<NutMap>> entry : grouped.entrySet()) {
List<NutMap> tasks = entry.getValue();
String displayName = tasks.get(0).getString("displayName", entry.getKey());
XWPFParagraph stageP = doc.createParagraph();
XWPFRun stageRun = stageP.createRun();
stageRun.setBold(true);
stageRun.setText(String.format("%d.%s", stageIndex++, displayName));
int idx = 1;
for (NutMap task : tasks) {
ProcessTaskVO vo = flowEngine.processTaskService().findById(task.getLong("id"));
Dict formData = vo == null ? null : vo.getTaskFormData();
XWPFTable table = doc.createTable(5, 2);
fillCell(table.getRow(0), "序号", String.valueOf(idx++));
fillCell(table.getRow(1), "办理人", extractUserName(formData, vo));
fillCell(table.getRow(2), "办理时间", task.getTime("finishTime") == null ? "" : DateUtil.formatDateTime(task.getTime("finishTime")));
fillCell(table.getRow(3), "办理意见", extractOpinion(formData));
fillCell(table.getRow(4), "环节数据", flattenFormData(formData));
}
}
}
private String extractUserName(Dict formData, ProcessTaskVO vo) {
String userName = "";
if (formData != null) {
userName = formData.getStr("userName");
if (StrUtil.isBlank(userName)) {
userName = formData.getStr("username");
}
}
if (StrUtil.isBlank(userName)) {
userName = vo == null ? "" : vo.getOperator();
}
return StrUtil.blankToDefault(userName, "");
}
private String extractOpinion(Dict formData) {
if (formData == null || formData.isEmpty()) {
return "";
}
List<String> keys = List.of("opinion", "auditOpinion", "feedback", "caseFilingResult", "caseFilingType", "tf_opinion", "tf_feedback", "tf_caseFilingResult", "tf_caseFilingType");
for (String key : keys) {
String val = formData.getStr(key);
if (StrUtil.isNotBlank(val)) {
return val;
}
}
return "";
}
private String flattenFormData(Dict formData) {
if (formData == null || formData.isEmpty()) {
return "";
}
Set<String> skipKeys = Set.of("tf_userName", "userName", "tf_loginName", "loginName", "tf_unitId", "unitId", "tf_unitName", "unitName", "tf_unionId", "unionId", "finishTimeForDocx");
return formData.entrySet().stream()
.filter(e -> !skipKeys.contains(e.getKey()))
.filter(e -> e.getValue() != null && StrUtil.isNotBlank(String.valueOf(e.getValue())))
.map(e -> e.getKey() + "=" + String.valueOf(e.getValue()))
.collect(Collectors.joining(""));
}
private void fillCell(XWPFTableRow row, String key, String value) {
row.getCell(0).setText(key);
row.getCell(1).setText(StrUtil.blankToDefault(value, ""));
}
}
@@ -0,0 +1,317 @@
package com.budwk.app.zhgh.democratic.proposal.controller.query;
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.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
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.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@IocBean
@At("/platform/proposal/query/analysis")
@Slf4j
@Ok("json:full")
@Api(tags = "提案统计分析")
public class ProposalQueryAnalysisController {
@Inject
private BaseService baseService;
@Inject
private SysDictService sysDictService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/query/analysis/index.html")
@SaCheckPermission("proposal.query.analysis")
public void index() {
}
@At
@ApiOperation("统计数据")
@SaCheckPermission("proposal.query.analysis")
public Result statisticsData(String sessionId) {
if (StrUtil.isBlank(sessionId)) {
return Result.error("请先选择教代会届次");
}
return Result.success(buildStatisticsRows(sessionId));
}
@At
@ApiOperation("分析数据")
@SaCheckPermission("proposal.query.analysis")
public Result analysisData(String sessionId) {
if (StrUtil.isBlank(sessionId)) {
return Result.error("请先选择教代会届次");
}
return Result.success(buildAnalysisRows(sessionId));
}
@At
@Ok("void")
@ApiOperation("导出统计数据")
@SaCheckPermission("proposal.query.analysis")
public void exportStatisticsData(String sessionId, HttpServletResponse response) {
List<NutMap> list = buildStatisticsRows(sessionId);
List<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("统计维度", "dimension", 20));
entities.add(new ExcelExportEntity("分类项", "itemName", 30));
entities.add(new ExcelExportEntity("数量", "count", 12));
entities.add(new ExcelExportEntity("占比", "rate", 12));
ExportParams params = new ExportParams();
params.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(params, entities, list);
CommonDownloadUtil.download("提案统计数据.xlsx", workbook, response);
}
@At
@Ok("void")
@ApiOperation("导出分析数据")
@SaCheckPermission("proposal.query.analysis")
public void exportAnalysisData(String sessionId, HttpServletResponse response) {
List<NutMap> list = buildAnalysisRows(sessionId);
List<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("统计维度", "dimension", 20));
entities.add(new ExcelExportEntity("总量", "total", 12));
entities.add(new ExcelExportEntity("分类数量", "categoryCount", 12));
entities.add(new ExcelExportEntity("最高项", "topItem", 30));
entities.add(new ExcelExportEntity("最高数量", "topCount", 12));
entities.add(new ExcelExportEntity("最高占比", "topRate", 12));
entities.add(new ExcelExportEntity("分析结论", "analysis", 60));
ExportParams params = new ExportParams();
params.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(params, entities, list);
CommonDownloadUtil.download("提案分析数据.xlsx", workbook, response);
}
private List<NutMap> buildStatisticsRows(String sessionId) {
Map<String, List<NutMap>> dataMap = loadDimensionData(sessionId);
List<NutMap> result = new ArrayList<>();
dataMap.forEach((dimension, items) -> {
long total = items.stream().mapToLong(v -> v.getLong("count", 0L)).sum();
for (NutMap item : items) {
long count = item.getLong("count", 0L);
result.add(NutMap.NEW()
.addv("dimension", dimension)
.addv("itemName", item.getString("itemName", "未维护"))
.addv("count", count)
.addv("rate", formatPercent(count, total)));
}
});
return result;
}
private List<NutMap> buildAnalysisRows(String sessionId) {
Map<String, List<NutMap>> dataMap = loadDimensionData(sessionId);
List<NutMap> result = new ArrayList<>();
dataMap.forEach((dimension, items) -> {
if (items.isEmpty()) {
result.add(NutMap.NEW()
.addv("dimension", dimension)
.addv("total", 0)
.addv("categoryCount", 0)
.addv("topItem", "-")
.addv("topCount", 0)
.addv("topRate", "0.00%")
.addv("analysis", "暂无数据"));
return;
}
List<NutMap> sorted = items.stream()
.sorted(Comparator.comparingLong((NutMap v) -> v.getLong("count", 0L)).reversed())
.toList();
long total = sorted.stream().mapToLong(v -> v.getLong("count", 0L)).sum();
NutMap top = sorted.get(0);
long topCount = top.getLong("count", 0L);
String topItem = top.getString("itemName", "未维护");
String topRate = formatPercent(topCount, total);
String analysis = String.format("%s主要集中在【%s】,数量为%d,占比%s。", dimension, topItem, topCount, topRate);
result.add(NutMap.NEW()
.addv("dimension", dimension)
.addv("total", total)
.addv("categoryCount", sorted.size())
.addv("topItem", topItem)
.addv("topCount", topCount)
.addv("topRate", topRate)
.addv("analysis", analysis));
});
return result;
}
private Map<String, List<NutMap>> loadDimensionData(String sessionId) {
Map<String, List<NutMap>> map = new LinkedHashMap<>();
map.put("提案人单位", queryUnitStat(sessionId));
map.put("提案类型", queryTypeStat(sessionId));
map.put("代表团", queryDelegationStat(sessionId));
map.put("立案结果", queryCaseFilingResultStat(sessionId));
map.put("满意度", querySatisfactionStat(sessionId));
return map;
}
private List<NutMap> queryUnitStat(String sessionId) {
Sql sql = Sqls.create("""
SELECT
IFNULL(tcde.unitName, '未维护') AS itemName,
COUNT(info.id) AS count
FROM proposal_info info
LEFT JOIN teacher_congress_delegate tcde ON tcde.loginName = info.createUserLoginName
WHERE info.sessionId = @sessionId
GROUP BY itemName
ORDER BY count DESC, itemName ASC
""");
sql.setParam("sessionId", sessionId);
return baseService.listMap(sql);
}
private List<NutMap> queryTypeStat(String sessionId) {
Sql sql = Sqls.create("""
SELECT
IFNULL(t.name, '未分类') AS itemName,
COUNT(info.id) AS count
FROM proposal_info info
LEFT JOIN proposal_type t ON t.id = info.typeId
WHERE info.sessionId = @sessionId
GROUP BY itemName
ORDER BY count DESC, itemName ASC
""");
sql.setParam("sessionId", sessionId);
return baseService.listMap(sql);
}
private List<NutMap> queryDelegationStat(String sessionId) {
Sql sql = Sqls.create("""
SELECT
IFNULL(d.name, '未分团') AS itemName,
COUNT(info.id) AS count
FROM proposal_info info
LEFT JOIN teacher_congress_delegation d ON d.id = info.delegationId
WHERE info.sessionId = @sessionId
GROUP BY itemName
ORDER BY count DESC, itemName ASC
""");
sql.setParam("sessionId", sessionId);
return baseService.listMap(sql);
}
private List<NutMap> queryCaseFilingResultStat(String sessionId) {
List<Sys_dict> dictList = sysDictService.getSubListByCode("PROPOSAL_CASE_FILING_RESULT");
Map<String, String> dictNameMap = dictList.stream().collect(Collectors.toMap(Sys_dict::getCode, Sys_dict::getName, (a, b) -> a));
Sql sql = Sqls.create("""
SELECT
IFNULL(info.caseFilingResult, '') AS code,
COUNT(info.id) AS count
FROM proposal_info info
WHERE info.sessionId = @sessionId
GROUP BY info.caseFilingResult
""");
sql.setParam("sessionId", sessionId);
List<NutMap> rawList = baseService.listMap(sql);
Map<String, Long> countMap = rawList.stream().collect(Collectors.toMap(v -> v.getString("code", ""), v -> v.getLong("count", 0L), Long::sum));
LinkedHashSet<String> allCodes = new LinkedHashSet<>(dictNameMap.keySet());
allCodes.addAll(countMap.keySet());
List<NutMap> result = new ArrayList<>();
for (String code : allCodes) {
String itemName = StrUtil.isBlank(code) ? "未立案结果" : dictNameMap.getOrDefault(code, code);
result.add(NutMap.NEW().addv("itemName", itemName).addv("count", countMap.getOrDefault(code, 0L)));
}
result.sort(Comparator.comparingLong((NutMap v) -> v.getLong("count", 0L)).reversed());
return result;
}
private List<NutMap> querySatisfactionStat(String sessionId) {
List<Sys_dict> dictList = sysDictService.getSubListByCode("PROPOSAL_FEEDBACK");
Map<String, String> dictNameMap = dictList.stream().collect(Collectors.toMap(Sys_dict::getCode, Sys_dict::getName, (a, b) -> a));
Sql totalSql = Sqls.create("SELECT COUNT(1) AS total FROM proposal_info WHERE sessionId = @sessionId");
totalSql.setParam("sessionId", sessionId);
List<NutMap> totalList = baseService.listMap(totalSql);
long total = totalList.isEmpty() ? 0L : totalList.get(0).getLong("total", 0L);
Sql sql = Sqls.create("""
SELECT
t.variable
FROM proposal_info info
LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id
LEFT JOIN (
SELECT processInstanceId, MAX(finishTime) AS finishTime
FROM wf_process_task
WHERE taskName = 'feedback' AND taskState = 20
GROUP BY processInstanceId
) latest ON latest.processInstanceId = ins.id
LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id
AND t.taskName = 'feedback'
AND t.taskState = 20
AND t.finishTime = latest.finishTime
WHERE info.sessionId = @sessionId
""");
sql.setParam("sessionId", sessionId);
List<NutMap> rowList = baseService.listMap(sql);
Map<String, Long> countMap = new LinkedHashMap<>();
long ratedCount = 0L;
for (NutMap row : rowList) {
String variableStr = row.getString("variable", "");
if (StrUtil.isBlank(variableStr)) {
continue;
}
NutMap variable = Json.fromJson(NutMap.class, variableStr);
String code = variable.getString("tf_feedback", "");
if (StrUtil.isBlank(code)) {
continue;
}
countMap.put(code, countMap.getOrDefault(code, 0L) + 1);
ratedCount++;
}
List<NutMap> result = new ArrayList<>();
for (Sys_dict dict : dictList) {
result.add(NutMap.NEW().addv("itemName", dict.getName()).addv("count", countMap.getOrDefault(dict.getCode(), 0L)));
}
countMap.forEach((code, cnt) -> {
if (!dictNameMap.containsKey(code)) {
result.add(NutMap.NEW().addv("itemName", code).addv("count", cnt));
}
});
long unrated = Math.max(total - ratedCount, 0L);
result.add(NutMap.NEW().addv("itemName", "未评价").addv("count", unrated));
result.sort(Comparator.comparingLong((NutMap v) -> v.getLong("count", 0L)).reversed());
return result;
}
private String formatPercent(long count, long total) {
if (total <= 0) {
return "0.00%";
}
return String.format("%.2f%%", (count * 100.0D) / total);
}
}
@@ -0,0 +1,118 @@
package com.budwk.app.zhgh.democratic.proposal.controller.transact;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.flow.enums.ProcessTaskPerformTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.democratic.proposal.param.ProposalSearchParam;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
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.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.validation.Valid;
import java.util.List;
@IocBean
@At("/platform/proposal/control")
@Slf4j
@Ok("json:full")
@Api(tags = "提案管理-状态调整")
public class ProposalControlController {
@Inject
private BaseService baseService;
@At("")
@Ok("beetl:/platform/zhgh/democratic/proposal/transact/control/index.html")
@SaCheckPermission("proposal.control")
public void index() {
}
@At
@SaCheckPermission("proposal.control")
@ApiOperation("分页列表")
public Result pageData(@Valid ProposalSearchParam pageForm) {
Sql sql = Sqls.create("""
SELECT
info.*,
type.name AS typeName,
tcs.fullName AS sessionName,
tcd.name AS delegationName,
ins.id AS instanceId,
ins.businessNo,
ins.state instanceState,
ins.variable instanceVariable,
ins.processDefineId instanceProcessDefineId,
t.id taskId,
t.taskName AS taskKey,
t.displayName taskName,
t.taskType,
t.performType taskPerformType,
t.taskState,
t.finishTime,
t.taskParentId,
t.variable taskVariable,
IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') curTaskName
FROM wf_process_task t
LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10
LEFT JOIN proposal_info info ON info.id = ins.businessNo
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
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode());
if (!AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) {
cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId()));
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
cnd.desc("t.createdAt");
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
}
@At
@SaCheckPermission("proposal.control")
@ApiOperation("获取可跳转节点")
public Result jumpNodes(Long instanceId) {
if (instanceId == null) {
return Result.error("参数错误");
}
Sql sql = Sqls.create("""
SELECT
t.taskName AS value,
MAX(t.displayName) AS label,
MIN(t.id) AS seq
FROM wf_process_task t
WHERE t.processInstanceId = @instanceId
AND t.taskState IN (@doing, @finish)
AND t.performType != @countersign
GROUP BY t.taskName
ORDER BY seq ASC
""");
sql.params().set("instanceId", instanceId);
sql.params().set("doing", ProcessTaskStateEnum.DOING.getCode());
sql.params().set("finish", ProcessTaskStateEnum.FINISHED.getCode());
sql.params().set("countersign", ProcessTaskPerformTypeEnum.COUNTERSIGN.getCode());
return Result.success(baseService.listMap(sql));
}
}
@@ -49,6 +49,14 @@ public interface ProposalExportService extends BaseService<ProposalInfo> {
* @param response
*/
void exportProposalStatisticsAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response);
/**
* 批量导出提案表为单个汇集文档
*
* @param pageForm
* @param response
*/
void exportProposalStatisticsAsDocx(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response);
/**
* 导出反馈表
@@ -28,6 +28,12 @@ import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xwpf.usermodel.BreakType;
import org.apache.poi.xwpf.usermodel.IBodyElement;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.ddr.poi.html.HtmlRenderPolicy;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
@@ -40,6 +46,7 @@ import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
@@ -326,15 +333,16 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
for (NutMap proposalRow : list) {
try (ByteArrayOutputStream docxByteArrayOutputStream = new ByteArrayOutputStream()) {
proposalCommonService.exportProposalAsDocx(proposalRow.getString("id"), docxByteArrayOutputStream);
ZipEntry zipEntry = new ZipEntry(proposalRow.getString("code") + "-" + proposalRow.getString("name") + ".docx");
ZipEntry zipEntry = new ZipEntry(proposalRow.getString("code") + "-" + sanitizeZipFileName(proposalRow.getString("name")) + ".docx");
zipOutputStream.putNextEntry(zipEntry);
docxByteArrayOutputStream.writeTo(zipOutputStream);
zipOutputStream.closeEntry();
} catch (IOException e) {
// 处理单个文件生成失败的情况
e.printStackTrace();
throw new RuntimeException("zip entry write failed, proposalId=" + proposalRow.getString("id"), e);
}
}
zipOutputStream.finish();
zipOutputStream.close();
CommonDownloadUtil.download("提案汇总压缩包.zip", bos.toByteArray(), response);
} catch (Exception e) {
@@ -345,6 +353,92 @@ public class ProposalExportServiceImpl extends BaseServiceImpl<ProposalInfo> imp
}
@Override
public void exportProposalStatisticsAsDocx(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) {
List<NutMap> list = queryProposalBaseList(pageForm);
if (Lang.isEmpty(list)) {
throw Lang.makeThrow("暂无可导出的提案数据");
}
XWPFDocument mergedDoc = null;
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
for (NutMap proposalRow : list) {
try (ByteArrayOutputStream oneDocOut = new ByteArrayOutputStream()) {
proposalCommonService.exportProposalAsDocx(proposalRow.getString("id"), oneDocOut);
if (mergedDoc == null) {
mergedDoc = new XWPFDocument(new ByteArrayInputStream(oneDocOut.toByteArray()));
} else {
try (XWPFDocument oneDoc = new XWPFDocument(new ByteArrayInputStream(oneDocOut.toByteArray()))) {
appendDocument(mergedDoc, oneDoc, true);
}
}
} catch (Exception e) {
log.error("proposal merge export failed, proposalId={}, error={}", proposalRow.getString("id"), e.getMessage());
}
}
if (mergedDoc == null) {
throw Lang.makeThrow("暂无可导出的提案数据");
}
mergedDoc.write(out);
CommonDownloadUtil.download("proposal-merged.docx", out.toByteArray(), response);
} catch (Exception e) {
log.error("proposal merge docx export failed: {}", e.getMessage(), e);
throw Lang.makeThrow("导出失败");
} finally {
if (mergedDoc != null) {
try {
mergedDoc.close();
} catch (IOException e) {
log.warn("close merged doc failed: {}", e.getMessage());
}
}
}
}
private List<NutMap> queryProposalBaseList(ProposalQueryComprehensiveParam pageForm) {
Sql sql = Sqls.create("""
SELECT
info.id,
info.code,
info.name
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
$condition
""");
Cnd cnd = Cnd.NEW();
ProposalQueryComprehensiveParam.buildSearch(cnd, pageForm);
cnd.groupBy("info.id");
cnd.asc("info.code");
sql.setCondition(cnd);
return listMap(sql);
}
private void appendDocument(XWPFDocument target, XWPFDocument source, boolean addPageBreakBefore) {
if (addPageBreakBefore && !target.getBodyElements().isEmpty()) {
XWPFParagraph paragraph = target.createParagraph();
XWPFRun run = paragraph.createRun();
run.addBreak(BreakType.PAGE);
}
for (IBodyElement bodyElement : source.getBodyElements()) {
if (bodyElement instanceof XWPFParagraph) {
XWPFParagraph paragraph = (XWPFParagraph) bodyElement;
XWPFParagraph newParagraph = target.createParagraph();
newParagraph.getCTP().set(paragraph.getCTP().copy());
} else if (bodyElement instanceof XWPFTable) {
XWPFTable table = (XWPFTable) bodyElement;
XWPFTable newTable = target.createTable();
newTable.getCTTbl().set(table.getCTTbl().copy());
}
}
}
private String sanitizeZipFileName(String name) {
return StrUtil.blankToDefault(name, "unnamed").replaceAll("[\\\\/:*?\"<>|\\r\\n]+", "_");
}
@Override
public void exportFeedBackAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) {
Sql sql = Sqls.create("""