福利补丁

This commit is contained in:
2026-08-29 17:21:53 +08:00
parent d19670324c
commit 0c7bd23869
10 changed files with 342 additions and 19 deletions
+9
View File
@@ -1,5 +1,14 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AliAccessStaticViaInstance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliArrayNamingShouldHaveBracket" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliControlFlowStatementWithoutBraces" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliDeprecation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliEqualsAvoidNull" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliLongLiteralsEndingWithLowercaseL" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliMissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliWrapperTypeEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MapOrSetKeyShouldOverrideHashCodeEquals" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
@@ -70,7 +70,8 @@ public class QsvSurveyController {
@SaCheckPermission("qsv.survey")
@ApiOperation("分页查询")
public Result pageData(@Valid PageForm pageForm, Integer year) {
Cnd cnd = Cnd.where("category", "in", new String[]{"SURVEY", "VOTE"});
// Cnd cnd = Cnd.where("category", "in", new String[]{"SURVEY", "VOTE"});
Cnd cnd = Cnd.NEW();
cnd.andEX("YEAR(startTime)", "=", year);
cnd.desc("category");
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
@@ -323,6 +323,11 @@ public class H5QsvQuizController {
}
}
// 非随机答题统一按题目配置顺序展示;随机答题保留抽题记录中的原始随机顺序。
if (!"RANDOM".equals(displayMode)) {
resultSubjects = qsvQuizService.sortSubjectsByOrder(resultSubjects);
}
dao.fetchLinks(resultSubjects, "options", Cnd.NEW().asc("sortNum"));
resultSubjects.forEach(v -> v.setUserSelectOptionIds(new ArrayList<>()));
@@ -4,6 +4,7 @@ package com.budwk.app.zhgh.dayofficework.qsv.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.qsv.dto.QsvCheckAnswerResult;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvSubject;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvUserAnswerRecord;
import java.util.List;
@@ -18,4 +19,12 @@ public interface QsvQuizService extends BaseService<QsvUserAnswerRecord> {
*/
QsvCheckAnswerResult calcScore(String subjectId, List<String> userSelectOptions);
/**
* 按题目配置顺序整理非随机答题题目。
*
* @param subjects 待排序的题目列表,题目顺序字段为 sortNum
* @return 新的题目列表,按 sortNum 升序、空顺序置后,同序时按题目ID稳定排序
*/
List<QsvSubject> sortSubjectsByOrder(List<QsvSubject> subjects);
}
@@ -10,6 +10,7 @@ import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
@@ -33,4 +34,20 @@ public class QsvQuizServiceImpl extends BaseServiceImpl<QsvUserAnswerRecord> imp
boolean equals = new HashSet<>(userSelectOptions).equals(new HashSet<>(correctOptionIds));
return QsvCheckAnswerResult.builder().isCorrect(equals).score(equals ? subject.getScore() : 0).build();
}
/**
* 返回按题目配置顺序排列的新列表,不修改答题记录中保存的题目ID顺序。
* sortNum 为空时排在最后,同一顺序按题目ID排序以保证多次查询结果稳定。
*
* @param subjects 待排序的题目列表
* @return 排序后的新题目列表
*/
@Override
public List<QsvSubject> sortSubjectsByOrder(List<QsvSubject> subjects) {
List<QsvSubject> sortedSubjects = subjects == null ? new ArrayList<>() : new ArrayList<>(subjects);
sortedSubjects.sort(Comparator
.comparing(QsvSubject::getSortNum, Comparator.nullsLast(Integer::compareTo))
.thenComparing(QsvSubject::getId, Comparator.nullsLast(String::compareTo)));
return sortedSubjects;
}
}
@@ -158,6 +158,24 @@ public class WelfareSelectionSituationController {
situationService.receiveXlsx(pageForm, response);
}
/**
* 按选择情况页面的查询条件导出签收单模版。
*
* @param pageForm 页面完整查询条件,包含项目、福利选项、人员信息、组织范围和选择状态
* @param response HTTP响应,返回XLSX格式的签收单模版文件流
*/
@At
@SaCheckPermission("welfare.selection.situation")
@Ok("void")
@ApiOperation("导出签收单模版")
public void exportReceiveTemplate(@Valid @Param("pageForm") WelfareSelectionSituationPageForm pageForm,
HttpServletResponse response) {
if (StrUtil.isBlank(pageForm.getProjectId())) {
throw new IllegalArgumentException("请选择福利项目");
}
situationService.exportReceiveTemplate(pageForm, response);
}
@At
@SaCheckLogin
public Result getMobileByUserId(String userId) {
@@ -23,4 +23,12 @@ public interface WelfareSelectionSituationService extends BaseService<WelfareLis
void exportXlsx(WelfareSelectionSituationPageForm pageForm, HttpServletResponse response);
void receiveXlsx(WelfareSelectionSituationPageForm pageForm, HttpServletResponse response);
/**
* 按选择情况页面的完整查询条件导出签收单模版。
*
* @param pageForm 页面查询条件,包含项目、福利选项、姓名、工号、组织、人员类型和选择状态
* @param response HTTP响应,返回按所属单位分组的XLSX工作簿文件流
*/
void exportReceiveTemplate(WelfareSelectionSituationPageForm pageForm, HttpServletResponse response);
}
@@ -6,6 +6,7 @@ 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.afterturn.easypoi.excel.export.ExcelExportService;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import com.budwk.app.base.constant.RoleConstant;
@@ -27,7 +28,9 @@ import com.budwk.app.zhgh.welfare.param.WelfareSelectionSituationPageForm;
import com.budwk.app.zhgh.welfare.service.WelfareSelectionSituationService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
@@ -350,4 +353,224 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
CommonDownloadUtil.download("领取表.xls", workbook, response);
}
/**
* 按选择情况页面的完整筛选条件导出签收单模版。
* 每个所属单位生成一个工作表,每十五人横向增加一组“序号、姓名、签字”列。
*
* @param pageForm 页面查询条件,后台会复用列表条件并叠加当前登录人的数据权限
* @param response HTTP响应,返回XLSX格式的签收单模版文件流
*/
@Override
public void exportReceiveTemplate(WelfareSelectionSituationPageForm pageForm, HttpServletResponse response) {
WelfareProject project = dao().fetch(WelfareProject.class, pageForm.getProjectId());
if (project == null) {
throw new BaseException("福利项目不存在");
}
Sql sql = Sqls.create("""
SELECT DISTINCT
t1.id,
t1.userId,
t1.welfareUnionName,
t1.welfareUnitName,
t4.username AS userName,
t4.loginname AS loginName
FROM
welfare_list t1
LEFT JOIN welfare_project_user_selection t2 ON t2.welfareId = t1.projectId AND t2.selectUserId = t1.userId
LEFT JOIN welfare_project_subject_option t3 ON t3.id = t2.selectOptionId
LEFT JOIN sys_user t4 ON t4.id = t1.userId
$condition
""");
Cnd cnd = buildSituationCondition(pageForm);
cnd.asc("t1.welfareUnionName");
cnd.asc("t1.welfareUnitName");
cnd.asc("t4.loginname");
sql.setCondition(cnd);
List<NutMap> queryPeople = listMap(sql);
// 福利选项关联或历史名单重复时,以人员ID保留首条,避免同一人在签收单中重复出现。
List<NutMap> people = new ArrayList<>(queryPeople.stream().collect(Collectors.toMap(
item -> StrUtil.blankToDefault(item.getString("userId"), item.getString("id")),
item -> item,
(first, duplicate) -> first,
LinkedHashMap::new
)).values());
if (people.isEmpty()) {
throw new BaseException("当前查询条件下没有可导出的人员");
}
// 同名单位可能属于不同工会,因此使用工会ID语义对应的名称与单位名称共同作为分组键。
Map<String, List<NutMap>> peopleByUnit = people.stream().collect(Collectors.groupingBy(
item -> StrUtil.blankToDefault(item.getString("welfareUnionName"), "未设置工会")
+ "\t"
+ StrUtil.blankToDefault(item.getString("welfareUnitName"), "未设置单位"),
LinkedHashMap::new,
Collectors.toList()
));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
try (Workbook workbook = exportParams.getType() == ExcelType.XSSF
? new XSSFWorkbook()
: new HSSFWorkbook()) {
Set<String> sheetNames = new HashSet<>();
String provideMonth = project.getProvideTimeStart() == null
? ""
: DateUtil.format(project.getProvideTimeStart(), "yyyy年M月");
peopleByUnit.forEach((groupKey, unitPeople) -> {
String[] groupNames = groupKey.split("\t", -1);
String unitName = groupNames[1];
String sheetName = createUniqueSheetName(unitName, sheetNames);
Sheet sheet = workbook.createSheet(sheetName);
createReceiveTemplateSheet(workbook, sheet, project.getName(), unitName, provideMonth, unitPeople);
});
CommonDownloadUtil.download(project.getName() + "签收单模版.xlsx", workbook, response);
} catch (Exception e) {
log.error("导出签收单模版失败", e);
throw new BaseException("导出签收单模版失败");
}
}
/**
* 创建一个所属单位的签收单工作表。
* 人员每十五人占用一组三列,不足十五人的最后一组仍保留十五行签字空间。
*/
private void createReceiveTemplateSheet(Workbook workbook, Sheet sheet, String projectName,
String unitName, String provideMonth, List<NutMap> people) {
final int peoplePerGroup = 15;
final int columnsPerGroup = 3;
// 默认保留左右两组三列;超过30人后,每增加15人再向右扩展一组三列。
int groupCount = Math.max(2, (people.size() + peoplePerGroup - 1) / peoplePerGroup);
int totalColumns = groupCount * columnsPerGroup;
CellStyle titleStyle = createCellStyle(workbook, 18, true, HorizontalAlignment.CENTER,
VerticalAlignment.CENTER, false);
CellStyle infoStyle = createCellStyle(workbook, 11, false, HorizontalAlignment.LEFT,
VerticalAlignment.CENTER, false);
infoStyle.setWrapText(true);
CellStyle headerStyle = createCellStyle(workbook, 12, true, HorizontalAlignment.CENTER,
VerticalAlignment.CENTER, true);
CellStyle bodyStyle = createCellStyle(workbook, 11, false, HorizontalAlignment.CENTER,
VerticalAlignment.CENTER, true);
Row titleRow = sheet.createRow(0);
titleRow.setHeightInPoints(32);
Cell titleCell = titleRow.createCell(0);
titleCell.setCellValue(StrUtil.blankToDefault(projectName, "福利项目"));
titleCell.setCellStyle(titleStyle);
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, totalColumns - 1));
Row infoRow = sheet.createRow(1);
infoRow.setHeightInPoints(32);
// 第二行按全部列的中点均分,左侧显示工会小组,右侧显示发放日期。
int infoSplitColumn = totalColumns / 2;
Cell unitCell = infoRow.createCell(0);
unitCell.setCellValue("工会小组(部门):" + unitName);
unitCell.setCellStyle(infoStyle);
if (infoSplitColumn > 1) {
sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, infoSplitColumn - 1));
}
Cell dateCell = infoRow.createCell(infoSplitColumn);
dateCell.setCellValue("发放日期:" + provideMonth);
dateCell.setCellStyle(infoStyle);
if (infoSplitColumn < totalColumns - 1) {
sheet.addMergedRegion(new CellRangeAddress(1, 1, infoSplitColumn, totalColumns - 1));
}
Row headerRow = sheet.createRow(2);
headerRow.setHeightInPoints(28);
for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) {
int startColumn = groupIndex * columnsPerGroup;
createCell(headerRow, startColumn, "序号", headerStyle);
createCell(headerRow, startColumn + 1, "姓名", headerStyle);
createCell(headerRow, startColumn + 2, "签字", headerStyle);
sheet.setColumnWidth(startColumn, 8 * 256);
sheet.setColumnWidth(startColumn + 1, 16 * 256);
sheet.setColumnWidth(startColumn + 2, 24 * 256);
}
for (int rowIndex = 0; rowIndex < peoplePerGroup; rowIndex++) {
Row dataRow = sheet.createRow(rowIndex + 3);
dataRow.setHeightInPoints(28);
for (int groupIndex = 0; groupIndex < groupCount; groupIndex++) {
int personIndex = groupIndex * peoplePerGroup + rowIndex;
int startColumn = groupIndex * columnsPerGroup;
if (personIndex < people.size()) {
NutMap person = people.get(personIndex);
createCell(dataRow, startColumn, personIndex + 1, bodyStyle);
createCell(dataRow, startColumn + 1, person.getString("userName", ""), bodyStyle);
} else {
createCell(dataRow, startColumn, "", bodyStyle);
createCell(dataRow, startColumn + 1, "", bodyStyle);
}
createCell(dataRow, startColumn + 2, "", bodyStyle);
}
}
sheet.setFitToPage(true);
sheet.setHorizontallyCenter(true);
PrintSetup printSetup = sheet.getPrintSetup();
printSetup.setPaperSize(PrintSetup.A4_PAPERSIZE);
printSetup.setLandscape(groupCount > 2);
printSetup.setFitWidth((short) 1);
printSetup.setFitHeight((short) 0);
workbook.setPrintArea(workbook.getSheetIndex(sheet), 0, totalColumns - 1, 0, peoplePerGroup + 2);
}
/**
* 创建统一的Excel单元格样式,边框参数用于区分标题信息与签收表格区域。
*/
private CellStyle createCellStyle(Workbook workbook, int fontSize, boolean bold,
HorizontalAlignment horizontalAlignment,
VerticalAlignment verticalAlignment, boolean bordered) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setFontName("宋体");
font.setFontHeightInPoints((short) fontSize);
font.setBold(bold);
style.setFont(font);
style.setAlignment(horizontalAlignment);
style.setVerticalAlignment(verticalAlignment);
if (bordered) {
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
}
return style;
}
/**
* 创建并赋值单元格,统一应用指定样式。
*/
private void createCell(Row row, int columnIndex, Object value, CellStyle style) {
Cell cell = row.createCell(columnIndex);
if (value instanceof Number number) {
cell.setCellValue(number.doubleValue());
} else {
cell.setCellValue(value == null ? "" : value.toString());
}
cell.setCellStyle(style);
}
/**
* 生成合法且不重复的工作表名称,处理Excel限制的特殊字符和31字符上限。
*/
private String createUniqueSheetName(String rawName, Set<String> usedNames) {
String baseName = StrUtil.blankToDefault(rawName, "未设置单位")
.replaceAll("[\\\\/?*\\[\\]:]", "_");
baseName = StrUtil.sub(baseName, 0, Math.min(baseName.length(), 31));
String sheetName = baseName;
int sequence = 2;
while (usedNames.contains(sheetName)) {
String suffix = "(" + sequence++ + ")";
int maxBaseLength = 31 - suffix.length();
sheetName = StrUtil.sub(baseName, 0, Math.min(baseName.length(), maxBaseLength)) + suffix;
}
usedNames.add(sheetName);
return sheetName;
}
}
@@ -92,6 +92,16 @@ layout("/layouts/platform.html"){
<el-button type="primary" size="small" icon="el-icon-download" style="margin-left: 10px" @click="receiveXlsx">
导出领取表
</el-button>
<el-button
type="primary"
size="small"
icon="el-icon-download"
style="margin-left: 10px"
:disabled="!pageForm.projectId"
@click="exportReceiveTemplate">
签收单模版导出
</el-button>
</table-tool>
<el-table
@@ -259,6 +269,17 @@ layout("/layouts/platform.html"){
this.$downLoad("/platform/welfare/selection/situation/receiveXlsx", { pageForm: JSON.stringify(this.pageForm) })
},
// 按当前页面全部查询条件导出签收单模版,后台会再次叠加登录人的数据权限。
exportReceiveTemplate() {
if (!this.pageForm.projectId) {
this.$message.warning("请先选择福利项目")
return
}
this.$downLoad("/platform/welfare/selection/situation/exportReceiveTemplate", {
pageForm: JSON.stringify(this.pageForm)
})
},
// 打开发送窗口时清空上次编辑内容,实际接收人由后台按当前查询条件重新计算。
openMessageDialog() {
if (!this.pageForm.projectId) {
@@ -93,19 +93,19 @@ layout("/layouts/platform.html"){
</el-select>
</search-item>
<search-item label="三级单位">
<el-select
clearable
filterable
multiple
collapse-tags
placeholder="请先选择所属单位"
style="width: 100%"
v-model="pageForm.threeUnitIds"
>
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in threeUnitOptions"></el-option>
</el-select>
</search-item>
<!-- <search-item label="三级单位">-->
<!-- <el-select-->
<!-- clearable-->
<!-- filterable-->
<!-- multiple-->
<!-- collapse-tags-->
<!-- placeholder="请先选择所属单位"-->
<!-- style="width: 100%"-->
<!-- v-model="pageForm.threeUnitIds"-->
<!-- >-->
<!-- <el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in threeUnitOptions"></el-option>-->
<!-- </el-select>-->
<!-- </search-item>-->
<search-item label="人员类型:">
<dict-select
@@ -129,10 +129,15 @@ layout("/layouts/platform.html"){
v-model="pageForm.userStates"
></dict-select>
</search-item>
<search-item label="人员分类">
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
<!-- <search-item label="人员分类">-->
<!-- <dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"-->
<!-- code="AIDFUND_MEMBER_USER_TYPE"></dict-select>-->
<!-- </search-item>-->
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" clearable multiple collapse-tags></dict-select>
</search-item>
<!-- <search-item label="所选福利:">-->
<!-- <el-select clearable placeholder="请选择所选福利" style="width: 100%" v-model="pageForm.optionId">-->
<!-- <el-option :key="item.id" :label="item.optionName" :value="item.id" v-for="item in pageFormProject"></el-option>-->
@@ -161,7 +166,14 @@ layout("/layouts/platform.html"){
>
添加人员
</el-button>
<el-button :disabled="!canSyncUnion" :loading="syncUnionLoading" @click="syncWelfareListUnitAndUnion" icon="el-icon-refresh" size="small" type="primary">
<el-button
v-if="$auth.hasRole('SYSADMIN') || $auth.hasRole('SCHOOL_UNION_ADMIN')"
:disabled="!canSyncUnion"
:loading="syncUnionLoading"
@click="syncWelfareListUnitAndUnion"
icon="el-icon-refresh"
size="small"
type="primary">
同步工会
</el-button>
</table-tool>