This commit is contained in:
2026-08-28 10:45:40 +08:00
parent fc9613197b
commit 6e5af96498
5413 changed files with 3578 additions and 803784 deletions
@@ -131,6 +131,20 @@ public class WelfareProject extends BaseModel implements SysHomeConvert {
@ColDefine(type = ColType.BOOLEAN)
private Boolean singleOptionSupportMultipleSelection;
/**
* 是否启用福利选项分组配置。启用后,项目管理页面显示分组选择数量配置入口。
*/
@Column
@Comment("是否启用分组必选")
@Default("0")
@ColDefine(type = ColType.BOOLEAN)
private Boolean groupRequired;
/**
* 分组选择数量配置,仅用于接口提交和回显,实际以 JSON 形式存储在福利项目题目表中。
*/
private List<WelfareProjectSubject.GroupSelectionConfig> groupSelectionConfigs;
@Column
@Comment("主福利礼品")
@@ -50,8 +50,35 @@ public class WelfareProjectSubject extends BaseModel implements Serializable {
@ColDefine(type = ColType.INT)
private String subjectMaxSelectNum;
/**
* 福利分组选择数量配置。一个福利项目使用一条题目记录,以 JSON 数组保存全部分组配置。
*/
@Column
@Comment("分组选择数量配置")
@ColDefine(type = ColType.MYSQL_JSON)
private List<GroupSelectionConfig> groupSelectionConfigs;
@Many(field = "subjectId")
private List<WelfareProjectSubjectOption> options;
/**
* 单个福利分组的选择数量范围。
*/
@Data
@Accessors(chain = true)
public static class GroupSelectionConfig implements Serializable {
private static final long serialVersionUID = 1L;
/** 分组名称,与福利选项的分组字段对应。 */
private String groupName;
/** 最少选择数量,默认值为 1。 */
private Integer minSelectNum;
/** 最多选择数量,0 表示不限制。 */
private Integer maxSelectNum;
}
}
@@ -47,6 +47,14 @@ public class WelfareProjectSubjectOption extends BaseModel implements Serializab
@ColDefine(type = ColType.VARCHAR, width = 50)
private String optionName;
/**
* 福利选项所属分组名称,为空时不参与分组选择数量配置。
*/
@Column
@Comment("分组名称")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String groupName;
@Column
@Comment("选项名称Id")
@ColDefine(type = ColType.VARCHAR, width = 50)
@@ -1,11 +1,13 @@
package com.budwk.app.zhgh.welfare.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.welfare.model.WelfareList;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
import com.budwk.app.zhgh.welfare.model.WelfareProjectSubject;
import com.budwk.app.zhgh.welfare.model.WelfareProjectSubjectOption;
import com.budwk.app.zhgh.welfare.model.WelfareUserSelection;
import com.budwk.app.zhgh.welfare.service.WelfareProjectService;
@@ -32,6 +34,12 @@ import java.util.stream.Collectors;
*/
@IocBean(args = {"refer:dao"})
public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> implements WelfareProjectService {
/** 福利分组配置在福利项目题目表中的类型标识。 */
private static final String GROUP_CONFIG_SUBJECT_TYPE = "groupConfig";
/** 福利分组配置记录的固定名称,便于后台识别和维护。 */
private static final String GROUP_CONFIG_SUBJECT_NAME = "分组选择数量配置";
private static final Logger log = LoggerFactory.getLogger(WelfareProjectServiceImpl.class);
public WelfareProjectServiceImpl(Dao dao) {
@@ -45,10 +53,27 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
return null;
}
fetchLinks(project, "options", Cnd.NEW().asc("optionSort"));
fillGroupSelectionConfigs(project);
fillOptionRank(projectId, project.getOptions());
return project;
}
/**
* 从福利项目题目表读取 JSON 分组配置并填充到项目返回值中。
*
* @param project 待填充分组配置的福利项目
*/
private void fillGroupSelectionConfigs(WelfareProject project) {
WelfareProjectSubject configSubject = dao().fetch(WelfareProjectSubject.class,
Cnd.where(WelfareProjectSubject::getProjectId, "=", project.getId())
.and(WelfareProjectSubject::getSubjectType, "=", GROUP_CONFIG_SUBJECT_TYPE));
List<WelfareProjectSubject.GroupSelectionConfig> configs = configSubject == null
|| configSubject.getGroupSelectionConfigs() == null
? new ArrayList<>()
: configSubject.getGroupSelectionConfigs();
project.setGroupSelectionConfigs(configs);
}
@Override
public List<WelfareProject> listHistoryProject() {
return dao().query(WelfareProject.class, Cnd.NEW().desc(WelfareProject::getYear).desc(WelfareProject::getChoiceTimeStart));
@@ -112,9 +137,12 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
@Override
@Aop(TransAop.READ_COMMITTED)
public void saveProject(WelfareProject project) {
// 保存前根据福利选项分组去重并校验选择数量范围,防止非法 JSON 配置进入数据库。
normalizeGroupSelectionConfigs(project);
// 插入项目
project.setYear(DateUtil.thisYear());
insertWith(project, "options");
saveGroupSelectionConfigs(project);
// 更新首页活动
Sys_home_activity sysHomeActivity = project.covertToSysHomeActivity();
@@ -124,6 +152,8 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateProject(WelfareProject project) {
// 修改项目时同样归一化分组配置,确保新增、编辑两条保存链路规则一致。
normalizeGroupSelectionConfigs(project);
// 更新项目
update(project);
List<WelfareProjectSubjectOption> optionList = dao().query(WelfareProjectSubjectOption.class, Cnd.where(WelfareProjectSubjectOption::getWelfareId, "=", project.getId()));
@@ -147,11 +177,16 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
Cnd.where(WelfareProjectSubjectOption::getId, "in", toDeleteIds));
}
for (WelfareProjectSubjectOption option : project.getOptions()) {
List<WelfareProjectSubjectOption> submittedOptions = project.getOptions() == null
? Collections.emptyList()
: project.getOptions();
for (WelfareProjectSubjectOption option : submittedOptions) {
option.setWelfareId(project.getId());
dao().insertOrUpdate(option);
}
saveGroupSelectionConfigs(project);
// 更新首页活动
Sys_home_activity sysHomeActivity = project.covertToSysHomeActivity();
dao().insertOrUpdate(sysHomeActivity);
@@ -162,6 +197,7 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
@Aop(TransAop.READ_COMMITTED)
public void deleteWelfare(String id) {
dao().clear(WelfareProjectSubjectOption.class, Cnd.where(WelfareProjectSubjectOption::getWelfareId, "=", id));
dao().clear(WelfareProjectSubject.class, Cnd.where(WelfareProjectSubject::getProjectId, "=", id));
dao().clear(WelfareUserSelection.class, Cnd.where(WelfareUserSelection::getWelfareId, "=", id));
dao().clear(WelfareList.class, Cnd.where(WelfareList::getProjectId, "=", id));
delete(id);
@@ -169,6 +205,72 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
dao().clear(Sys_home_activity.class, Cnd.where("id", "=", id));
}
/**
* 根据福利选项中的非空分组生成最终配置数组,并校验最小、最大选择数量。
* 最大选择数量为 0 时表示不限,不参与最小值与最大值的大小比较。
*
* @param project 待保存的福利项目及其福利选项、分组配置
*/
private void normalizeGroupSelectionConfigs(WelfareProject project) {
List<WelfareProjectSubjectOption> options = Objects.requireNonNullElse(project.getOptions(), Collections.emptyList());
LinkedHashSet<String> optionGroupNames = new LinkedHashSet<>();
for (WelfareProjectSubjectOption option : options) {
if (option == null || StrUtil.isBlank(option.getGroupName())) {
continue;
}
String groupName = option.getGroupName().trim();
option.setGroupName(groupName);
optionGroupNames.add(groupName);
}
Map<String, WelfareProjectSubject.GroupSelectionConfig> submittedConfigMap = new LinkedHashMap<>();
List<WelfareProjectSubject.GroupSelectionConfig> submittedConfigs = Objects.requireNonNullElse(
project.getGroupSelectionConfigs(), Collections.emptyList());
for (WelfareProjectSubject.GroupSelectionConfig config : submittedConfigs) {
if (config != null && StrUtil.isNotBlank(config.getGroupName())) {
submittedConfigMap.put(config.getGroupName().trim(), config);
}
}
List<WelfareProjectSubject.GroupSelectionConfig> normalizedConfigs = new ArrayList<>();
for (String groupName : optionGroupNames) {
WelfareProjectSubject.GroupSelectionConfig config = submittedConfigMap.getOrDefault(groupName,
new WelfareProjectSubject.GroupSelectionConfig());
int minSelectNum = Objects.requireNonNullElse(config.getMinSelectNum(), 1);
int maxSelectNum = Objects.requireNonNullElse(config.getMaxSelectNum(), 0);
if (minSelectNum < 0 || maxSelectNum < 0) {
throw new IllegalArgumentException("分组“" + groupName + "”的选择数量不能小于0");
}
if (maxSelectNum > 0 && minSelectNum > maxSelectNum) {
throw new IllegalArgumentException("分组“" + groupName + "”的最小选择数量不能大于最大选择数量");
}
config.setGroupName(groupName);
config.setMinSelectNum(minSelectNum);
config.setMaxSelectNum(maxSelectNum);
normalizedConfigs.add(config);
}
project.setGroupSelectionConfigs(normalizedConfigs);
}
/**
* 将项目级分组选择数量数组写入福利项目题目表的 JSON 字段。
*
* @param project 已取得项目 ID 且完成配置归一化的福利项目
*/
private void saveGroupSelectionConfigs(WelfareProject project) {
WelfareProjectSubject configSubject = dao().fetch(WelfareProjectSubject.class,
Cnd.where(WelfareProjectSubject::getProjectId, "=", project.getId())
.and(WelfareProjectSubject::getSubjectType, "=", GROUP_CONFIG_SUBJECT_TYPE));
if (configSubject == null) {
configSubject = new WelfareProjectSubject();
configSubject.setProjectId(project.getId());
configSubject.setSubjectType(GROUP_CONFIG_SUBJECT_TYPE);
configSubject.setSubjectName(GROUP_CONFIG_SUBJECT_NAME);
}
configSubject.setGroupSelectionConfigs(Objects.requireNonNullElse(project.getGroupSelectionConfigs(), Collections.emptyList()));
dao().insertOrUpdate(configSubject);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void clearWelfareList(String projectId) {
+4 -4
View File
@@ -1,6 +1,6 @@
nutz:
application:
name: zhgh_cug_v4
name: zhgh_zjvtit_v4
mvc:
ignore: ^(.+[.])(jsp|png|gif|jpg|js|css|jspx|jpeg|html|mp3|mp4|ico|svg|vue)$
exclusions: /favicon/*,/assets/*,/druid/*,/upload/*,/components/*
@@ -21,7 +21,7 @@ jetty:
#结合ftp使用,或用nginx代理ftp路径
#staticPath: /Users/wizzer/temp/files
#开发模式静态资源
staticPathLocal: E:/projects/zhgh_cug_v4/src/main/resources/static
staticPathLocal: F:/coding/zhgh_zjvtit_v4/src/main/resources/static
security:
tokenName: saToken
@@ -56,7 +56,7 @@ cas:
redis:
host: 127.0.0.1
port: 6378
port: 6379
timeout: 2000
max_redir: 10
database: 1
@@ -87,7 +87,7 @@ xsssql:
beetl:
RESOURCE:
#本地路径,开发时设置本地路径,便于调试
rootLocal: E:/projects/zhgh_cug_v4/src/main/resources/views/
rootLocal: F:/coding/zhgh_zjvtit_v4/src/main/resources/views/
root: views/
DELIMITER_STATEMENT_START: "<!--#"
DELIMITER_STATEMENT_END: "#-->"
@@ -220,7 +220,13 @@ layout("/layouts/platform.html"){
<template v-if="formData.flexible">
<el-form-item label="福利选项">
<welfare-option :key="welfareOptionKey" v-model="formData.options"></welfare-option>
<welfare-option
ref="welfareOption"
:key="welfareOptionKey"
:group-required.sync="formData.groupRequired"
:group-selection-configs.sync="formData.groupSelectionConfigs"
v-model="formData.options">
</welfare-option>
</el-form-item>
</template>
@@ -313,14 +319,15 @@ layout("/layouts/platform.html"){
async save() {
const formValid = await this.$refs["form"].validate()
if (formValid) {
if (this.formData.flexible) {
// 弹性福利提交前同步并校验分组数量配置,避免保存过期或非法的分组数据。
if (this.formData.flexible && this.$refs.welfareOption && !this.$refs.welfareOption.validate()) {
return
}
const formData = clone(this.formData)
formData.provideAddress = "工会"
try {
this.submitLoading = true
const resp = await this.$axios.post(loc() + "/save", { data: JSON.stringify(formData) })
this.submitLoading = true
this.$axios.post(loc() + "/save", { data: JSON.stringify(formData) }).then((resp) => {
if (resp.code === 0) {
this.$refs.guava.index()
this.doSearch()
@@ -328,11 +335,11 @@ layout("/layouts/platform.html"){
} else {
this.$message.error(resp.msg)
}
} catch (e) {
}).catch((e) => {
this.notifyWarning(e.status)
} finally {
}).finally(() => {
this.submitLoading = false
}
})
}
},
isCheckBoxChange() {
@@ -388,6 +395,9 @@ layout("/layouts/platform.html"){
defaultFlexibleGifts: 0,
flexible: false,
noticePushMode: 1,
groupRequired: false,
groupSelectionConfigs: [],
options: [],
welfareProjectSubjects: [],
flexibleGifts: [{}]
})
@@ -481,7 +491,10 @@ layout("/layouts/platform.html"){
const data = resp.data
data.provideTime = [data.provideTimeStart, data.provideTimeEnd]
data.choiceTime = [data.choiceTimeStart, data.choiceTimeEnd]
data.groupRequired = Boolean(data.groupRequired)
data.groupSelectionConfigs = data.groupSelectionConfigs || []
this.formData = data
this.welfareOptionKey = this.welfareOptionKey + 1
this.$refs.guava.edit()
}
},
@@ -2,6 +2,24 @@ const welfareOption = {
template: /*language=HTML*/ `
<div class="welfare-option">
<el-card shadow="never">
<div class="welfare-option-toolbar">
<div class="group-required-control">
<span>分组必选</span>
<el-switch
v-model="localGroupRequired"
@change="onGroupRequiredChange">
</el-switch>
</div>
<el-button
v-if="localGroupRequired"
type="primary"
plain
icon="el-icon-setting"
@click="openGroupConfigDialog">
配置分组选择数量
</el-button>
</div>
<el-table :data="welfareList" border style="width: 100%; margin-top: 20px;">
<el-table-column align="center" header-align="center" label="福利名称" min-width="520">
<template slot-scope="{row}">
@@ -12,6 +30,16 @@ const welfareOption = {
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="分组" width="160">
<template slot-scope="{row}">
<el-input
v-model="row.groupName"
maxlength="50"
placeholder="请输入分组">
</el-input>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="系统默认选择" width="140">
<template slot-scope="{row}">
<el-radio
@@ -72,6 +100,46 @@ const welfareOption = {
<el-button type="primary" @click="saveDescription">确定</el-button>
</span>
</el-dialog>
<el-dialog
title="配置分组选择数量"
:visible.sync="groupConfigDialog.visible"
width="52%"
custom-class="welfare-group-config-dialog"
:append-to-body="true"
:close-on-click-modal="false">
<el-table :data="groupConfigDialog.configs" border class="group-config-table">
<el-table-column align="center" header-align="center" label="分组数据" prop="groupName">
</el-table-column>
<el-table-column align="center" header-align="center" label="最少选择数量" width="250">
<template slot-scope="{row}">
<el-input-number
v-model="row.minSelectNum"
:min="0"
:max="999"
controls-position="right">
</el-input-number>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="最多选择数量" width="250">
<template slot-scope="{row}">
<el-input-number
v-model="row.maxSelectNum"
:min="0"
:max="999"
controls-position="right">
</el-input-number>
</template>
</el-table-column>
</el-table>
<div v-if="groupConfigDialog.configs.length === 0" class="group-config-empty">
请先在福利选项中填写分组
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="groupConfigDialog.visible = false">取消</el-button>
<el-button type="primary" @click="saveGroupSelectionConfigs">确定</el-button>
</span>
</el-dialog>
</div>
`,
@@ -79,6 +147,14 @@ const welfareOption = {
value: {
type: Array,
default: () => []
},
groupRequired: {
type: Boolean,
default: false
},
groupSelectionConfigs: {
type: Array,
default: () => []
}
},
@@ -86,16 +162,22 @@ const welfareOption = {
return {
welfareList: [],
systemDefaultOption: null,
localGroupRequired: false,
descriptionDialog: {
visible: false,
content: '',
currentIndex: -1
},
groupConfigDialog: {
visible: false,
configs: []
}
}
},
created() {
this.welfareList = this.value ? JSON.parse(JSON.stringify(this.value)) : [];
this.localGroupRequired = Boolean(this.groupRequired);
this.systemDefaultOption = this.welfareList.find((item) => item.isSystemDefault) || null;
if (this.systemDefaultOption) {
this.setSystemDefault(this.systemDefaultOption);
@@ -108,6 +190,9 @@ const welfareOption = {
this.$emit('input', newVal);
},
deep: true
},
groupRequired(value) {
this.localGroupRequired = Boolean(value);
}
},
@@ -119,10 +204,87 @@ const welfareOption = {
optionSort: newSort,
imgUrl: '',
description: '',
groupName: '',
isSystemDefault: false
});
},
// 分组必选开关只负责配置入口显隐,并将当前状态同步给项目表单。
onGroupRequiredChange(value) {
this.$emit('update:groupRequired', value);
},
// 从福利选项中提取非空分组,按首次出现顺序去重。
getUniqueGroupNames() {
const groupNames = [];
const groupNameSet = new Set();
this.welfareList.forEach((item) => {
const groupName = (item.groupName || '').trim();
if (groupName && !groupNameSet.has(groupName)) {
groupNameSet.add(groupName);
groupNames.push(groupName);
}
});
return groupNames;
},
// 使用已保存配置回填当前分组;新出现的分组使用最小 1、最大 0 的默认值。
buildCurrentGroupConfigs() {
const savedConfigMap = new Map();
(this.groupSelectionConfigs || []).forEach((config) => {
if (config && config.groupName) {
savedConfigMap.set(config.groupName.trim(), config);
}
});
return this.getUniqueGroupNames().map((groupName) => {
const savedConfig = savedConfigMap.get(groupName) || {};
return {
groupName: groupName,
minSelectNum: savedConfig.minSelectNum == null ? 1 : Number(savedConfig.minSelectNum),
maxSelectNum: savedConfig.maxSelectNum == null ? 0 : Number(savedConfig.maxSelectNum)
};
});
},
openGroupConfigDialog() {
this.groupConfigDialog.configs = this.buildCurrentGroupConfigs();
this.groupConfigDialog.visible = true;
},
// 最大值为 0 时表示不限;填写具体最大值后,最小值不得超过最大值。
validateGroupSelectionConfigs(configs) {
const invalidConfig = configs.find((config) => {
return config.maxSelectNum > 0 && config.minSelectNum > config.maxSelectNum;
});
if (invalidConfig) {
this.$message.warning('分组“' + invalidConfig.groupName + '”的最小选择数量不能大于最大选择数量');
return false;
}
return true;
},
saveGroupSelectionConfigs() {
if (!this.validateGroupSelectionConfigs(this.groupConfigDialog.configs)) {
return;
}
const configs = JSON.parse(JSON.stringify(this.groupConfigDialog.configs));
this.$emit('update:groupSelectionConfigs', configs);
this.groupConfigDialog.visible = false;
},
// 项目提交前同步当前去重分组,保证未打开弹窗的新分组也能按默认值保存。
validate() {
if (!this.localGroupRequired) {
return true;
}
const configs = this.buildCurrentGroupConfigs();
if (!this.validateGroupSelectionConfigs(configs)) {
return false;
}
this.$emit('update:groupSelectionConfigs', configs);
return true;
},
// 每个福利项目只允许配置一个系统默认选项,保存时会随福利选项一并提交。
setSystemDefault(defaultOption) {
this.welfareList.forEach((item) => {
@@ -193,6 +355,21 @@ const welfareOption = {
padding: 0;
}
/deep/ .welfare-option .welfare-option-toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 28px;
min-height: 44px;
padding: 10px 16px 0;
}
/deep/ .welfare-option .group-required-control {
display: flex;
align-items: center;
gap: 12px;
}
/deep/ .welfare-option .imgUrl .el-upload--picture-card i {
position: absolute;
top: 50%;
@@ -210,5 +387,25 @@ const welfareOption = {
display: block;
object-fit: cover;
}
/deep/ .group-config-table .el-input-number {
width: 180px;
}
/deep/ .welfare-group-config-dialog {
display: flex;
flex-direction: column;
min-height: 560px;
}
/deep/ .welfare-group-config-dialog .el-dialog__body {
flex: 1;
}
/deep/ .group-config-empty {
padding-top: 20px;
color: #909399;
text-align: center;
}
`
};