职工福利功能优化-1.1

This commit is contained in:
2026-09-06 17:40:30 +08:00
parent b87567230f
commit d605391fa5
7 changed files with 141 additions and 46 deletions
@@ -1,6 +1,5 @@
package com.budwk.app.zhgh.welfare.controller; package com.budwk.app.zhgh.welfare.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.welfare.service.WelfareProjectService; import com.budwk.app.zhgh.welfare.service.WelfareProjectService;
@@ -25,11 +24,10 @@ public class WelfareNoticeController {
@Inject @Inject
private WelfareProjectService projectService; private WelfareProjectService projectService;
@At("detail") @At
@Ok("beetl:/platform/zhghh5/welfare/notice/detail.html") @Ok("beetl:/platform/zhghh5/welfare/notice/detail.html")
@SaCheckLogin
@ApiOperation("福利通知附件详情页") @ApiOperation("福利通知附件详情页")
public void detail() { public void index() {
} }
/** /**
@@ -38,8 +36,7 @@ public class WelfareNoticeController {
* @param id 福利项目 ID * @param id 福利项目 ID
* @return JSON 结果,data 中包含项目名称、文件名称、下载地址和 PDF 预览地址 * @return JSON 结果,data 中包含项目名称、文件名称、下载地址和 PDF 预览地址
*/ */
@At("detailData") @At
@SaCheckLogin
@ApiOperation("查询福利通知附件详情") @ApiOperation("查询福利通知附件详情")
public Result detailData(@Valid String id) { public Result detailData(@Valid String id) {
if (StrUtil.isBlank(id)) { if (StrUtil.isBlank(id)) {
@@ -8,7 +8,6 @@ import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.welfare.model.WelfareList; import com.budwk.app.zhgh.welfare.model.WelfareList;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
import com.budwk.app.zhgh.welfare.model.WelfareUserSelection; import com.budwk.app.zhgh.welfare.model.WelfareUserSelection;
import com.budwk.app.zhgh.welfare.service.WelfareProjectService; import com.budwk.app.zhgh.welfare.service.WelfareProjectService;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
@@ -24,10 +23,8 @@ import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects;
@IocBean @IocBean
@Ok("json:full") @Ok("json:full")
@@ -92,8 +89,6 @@ public class WelfareUserSelectController {
if (count == 0) { if (count == 0) {
return Result.error("您没有选择的权限"); return Result.error("您没有选择的权限");
} }
WelfareProject project = dao.fetch(WelfareProject.class, projectId);
Date selectTime = new Date(); Date selectTime = new Date();
String additionalInfoValidationMessage = welfareProjectService.validateSelectionAdditionalInfo(selections, selectTime); String additionalInfoValidationMessage = welfareProjectService.validateSelectionAdditionalInfo(selections, selectTime);
if (additionalInfoValidationMessage != null) { if (additionalInfoValidationMessage != null) {
@@ -111,9 +106,9 @@ public class WelfareUserSelectController {
return Result.error(groupQuantityValidationMessage); return Result.error(groupQuantityValidationMessage);
} }
int sum = Arrays.stream(selections).mapToInt(selection -> Objects.requireNonNullElse(selection.getSelectNum(), 0)).sum(); String projectQuantityValidationMessage = welfareProjectService.validateProjectSelectionQuantity(projectId, selections);
if (sum > project.getMultiSelectNum()) { if (projectQuantityValidationMessage != null) {
return Result.error("选择的数量不能超过" + project.getMultiSelectNum()); return Result.error(projectQuantityValidationMessage);
} }
@@ -52,6 +52,15 @@ public interface WelfareProjectService extends BaseService<WelfareProject> {
*/ */
String validateGroupSelectionQuantity(String projectId, WelfareUserSelection[] selections); String validateGroupSelectionQuantity(String projectId, WelfareUserSelection[] selections);
/**
* 校验福利项目一次提交的总选择份数。
*
* @param projectId 福利项目 ID,用于读取单选或多选模式及多选数量上限
* @param selections 用户提交的福利选择数组,每项需包含 selectOptionId 和 selectNum
* @return 校验通过返回 {@code null};超过项目上限或多选项目未配置上限时返回错误提示
*/
String validateProjectSelectionQuantity(String projectId, WelfareUserSelection[] selections);
/** /**
* 校验福利选择附加信息。同一次提交的全部福利项必须使用相同备注,配送时间字段已停用。 * 校验福利选择附加信息。同一次提交的全部福利项必须使用相同备注,配送时间字段已停用。
* *
@@ -201,6 +201,40 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
return null; return null;
} }
/**
* 校验福利项目总选择份数。单选项目固定只能选择 1 份,不读取仅供多选项目使用的
* multiSelectNum;多选项目必须配置有效的数量上限。
*
* @param projectId 福利项目 ID
* @param selections 当前用户提交的福利选择数据
* @return 校验通过返回 {@code null},否则返回可直接展示的错误提示
*/
@Override
public String validateProjectSelectionQuantity(String projectId, WelfareUserSelection[] selections) {
WelfareProject project = fetch(projectId);
if (project == null) {
return "福利项目不存在";
}
int selectedQuantity = Arrays.stream(selections == null ? new WelfareUserSelection[0] : selections)
.filter(Objects::nonNull)
.mapToInt(selection -> Objects.requireNonNullElse(selection.getSelectNum(), 0))
.sum();
boolean multipleSelection = "checkBox".equals(project.getIsCheckBox());
int maxSelectQuantity;
if (multipleSelection) {
if (project.getMultiSelectNum() == null || project.getMultiSelectNum() <= 0) {
return "福利项目未配置最多选择数量";
}
maxSelectQuantity = project.getMultiSelectNum();
} else {
maxSelectQuantity = 1;
}
if (selectedQuantity > maxSelectQuantity) {
return "选择的数量不能超过" + maxSelectQuantity;
}
return null;
}
/** /**
* 校验一次选择提交中的备注。配送时间字段已停用,新提交数据统一清空该字段; * 校验一次选择提交中的备注。配送时间字段已停用,新提交数据统一清空该字段;
* 多项福利的备注必须一致,避免同一次选择出现互相冲突的附加信息。 * 多项福利的备注必须一致,避免同一次选择出现互相冲突的附加信息。
@@ -145,7 +145,7 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
String title = project.getName() + "-福利通知"; String title = project.getName() + "-福利通知";
String detailUrl = StrUtil.removeSuffix(Globals.AppDomain, "/") String detailUrl = StrUtil.removeSuffix(Globals.AppDomain, "/")
+ "/platform/welfare/notice/detail?id=" + project.getId(); + "/platform/welfare/notice/index?id=" + project.getId();
String detailLine = "查看详情:" + detailUrl; String detailLine = "查看详情:" + detailUrl;
// 移除前端预置的详情行后统一追加到正文末尾,既避免重复链接,也保证最终消息排版一致。 // 移除前端预置的详情行后统一追加到正文末尾,既避免重复链接,也保证最终消息排版一致。
String messageBody = Arrays.stream(plainContent.split("\\R")) String messageBody = Arrays.stream(plainContent.split("\\R"))
@@ -153,7 +153,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
.collect(Collectors.joining("\n")) .collect(Collectors.joining("\n"))
.trim(); .trim();
String sendContent = StrUtil.isBlank(messageBody) ? detailLine : messageBody + "\n" + detailLine; String sendContent = StrUtil.isBlank(messageBody) ? detailLine : messageBody + "\n" + detailLine;
boolean success = smsService.sendMsg("6", loginNames, null, title, sendContent, detailUrl, detailUrl); // 详情地址已经包含在消息正文中,跳转参数保持为空,避免消息中心再次追加 PC、移动端详情链接。
boolean success = smsService.sendMsg("6", loginNames, null, title, sendContent, null, null);
if (!success) { if (!success) {
throw new BaseException("钉钉消息发送失败,请检查消息发送配置或稍后重试"); throw new BaseException("钉钉消息发送失败,请检查消息发送配置或稍后重试");
} }
@@ -243,7 +243,7 @@ layout("/layouts/platform.html"){
return "" return ""
} }
const appDomain = typeof APP_DOMAIN !== "undefined" && APP_DOMAIN ? APP_DOMAIN : window.location.origin const appDomain = typeof APP_DOMAIN !== "undefined" && APP_DOMAIN ? APP_DOMAIN : window.location.origin
return appDomain.replace(/\/+$/, "") + "/platform/welfare/notice/detail?id=" + encodeURIComponent(this.pageForm.projectId) return appDomain.replace(/\/+$/, "") + "/platform/welfare/notice/index?id=" + encodeURIComponent(this.pageForm.projectId)
}, },
welfareOptions() { welfareOptions() {
if (this.pageForm.projectId) { if (this.pageForm.projectId) {
@@ -242,7 +242,8 @@ layout("/layouts/platform_h5.html"){
.welfare-detail-link { .welfare-detail-link {
display: inline-flex; display: inline-flex;
height: 20px; height: 20px;
margin-left: 6px; margin-left: auto;
padding-left: 6px;
flex: none; flex: none;
align-items: center; align-items: center;
color: #1989fa; color: #1989fa;
@@ -827,8 +828,8 @@ layout("/layouts/platform_h5.html"){
<span>{{ optionRankText(option) }}</span> <span>{{ optionRankText(option) }}</span>
</div> </div>
<div class="welfare-option-desc-row" v-if="optionBrief(option)"> <div class="welfare-option-desc-row">
<div class="welfare-option-desc"> <div class="welfare-option-desc" v-if="optionBrief(option)">
{{ optionBrief(option) }} {{ optionBrief(option) }}
</div> </div>
<div class="welfare-detail-link" @click.stop="showOptionDetail(option)"> <div class="welfare-detail-link" @click.stop="showOptionDetail(option)">
@@ -847,7 +848,8 @@ layout("/layouts/platform_h5.html"){
integer integer
disable-input disable-input
:min="option.isSystemDefault ? 1 : 0" :min="option.isSystemDefault ? 1 : 0"
:max="projectInfo.isCheckBox === 'radio' ? 1 : selectionLimit" :max="getOptionMaxSelectNum(option)"
:disable-plus="isOptionIncreaseDisabled(option)"
:disabled="isDeadlinePassed || (projectInfo.isCheckBox === 'radio' && systemDefaultOption && systemDefaultOption.id !== option.id)" :disabled="isDeadlinePassed || (projectInfo.isCheckBox === 'radio' && systemDefaultOption && systemDefaultOption.id !== option.id)"
input-width="40px" input-width="40px"
button-size="22px" button-size="22px"
@@ -984,9 +986,11 @@ layout("/layouts/platform_h5.html"){
<van-action-sheet v-model="showOptionDetailDialog" class="welfare-detail-sheet" <van-action-sheet v-model="showOptionDetailDialog" class="welfare-detail-sheet"
:style="{ height: '78%' }" :close-on-click-overlay="true" :style="{ height: '78%' }" :close-on-click-overlay="true"
:title="selectedOption ? selectedOption.optionName : ''" cancel-text="取消" :title="selectedOption ? selectedOption.optionName : ''" cancel-text="取消"
@close="handleHistoryLayerComponentClose('detail')"> @close="handleHistoryLayerComponentClose('detail')">
<div class="welfare-detail-popup" v-if="selectedOption"> <div class="welfare-detail-popup" v-if="selectedOption">
<div class="welfare-detail-content" v-html="selectedOption.description"></div> <div class="welfare-detail-content" v-if="hasOptionDetail(selectedOption)"
v-html="selectedOption.description"></div>
<van-empty v-else description="暂无详情"></van-empty>
</div> </div>
</van-action-sheet> </van-action-sheet>
@@ -1196,6 +1200,20 @@ layout("/layouts/platform_h5.html"){
const text = (div.textContent || div.innerText || "").replace(/\s+/g, " ").trim() const text = (div.textContent || div.innerText || "").replace(/\s+/g, " ").trim()
return text return text
}, },
/**
* 判断福利选项是否存在可展示的详情内容。
* @param option 福利选项,详情来源为 description 字段。
* @returns {boolean} 存在文字或富媒体内容时返回 true,否则详情弹框显示“暂无详情”。
*/
hasOptionDetail(option) {
if (!option || !option.description) {
return false
}
const div = document.createElement("div")
div.innerHTML = option.description
const text = (div.textContent || div.innerText || "").replace(/\s+/g, " ").trim()
return !!text || !!div.querySelector("img,table,video,audio,iframe")
},
selectNumChange(index, newValue) { selectNumChange(index, newValue) {
const option = this.projectInfo.options[index]; const option = this.projectInfo.options[index];
if (option.isSystemDefault && Number(newValue) < 1) { if (option.isSystemDefault && Number(newValue) < 1) {
@@ -1203,6 +1221,12 @@ layout("/layouts/platform_h5.html"){
this.$toast.fail("系统默认福利不可取消") this.$toast.fail("系统默认福利不可取消")
return return
} }
if (!this.projectInfo.singleOptionSupportMultipleSelection && Number(newValue) > 1) {
this.$set(option, "selectNum", 1)
this.$set(option, "selectNumKey", (option.selectNumKey || 0) + 1)
this.$toast.fail("此套餐最多只能选择1份")
return
}
const maxSelect = this.projectInfo.multiSelectNum || this.projectInfo.options.length; const maxSelect = this.projectInfo.multiSelectNum || this.projectInfo.options.length;
// 计算其他所有选项的总和(不包括当前修改的选项) // 计算其他所有选项的总和(不包括当前修改的选项)
@@ -1223,7 +1247,7 @@ layout("/layouts/platform_h5.html"){
// 如果当前选项的值大于允许的最大值,则调整它 // 如果当前选项的值大于允许的最大值,则调整它
if (option.selectNum > maxAllowedForThisOption) { if (option.selectNum > maxAllowedForThisOption) {
option.selectNum = maxAllowedForThisOption; this.$set(option, "selectNum", maxAllowedForThisOption)
} }
// 如果当前选项的值小于或等于允许的最大值(例如,用户减少了数量导致总数超标,但实际上减少后并未超过 maxSelect, // 如果当前选项的值小于或等于允许的最大值(例如,用户减少了数量导致总数超标,但实际上减少后并未超过 maxSelect,
// 这种情况理论上不应该在 change 时发生,因为 change 是值改变后触发的,且 change 后的值导致了超标), // 这种情况理论上不应该在 change 时发生,因为 change 是值改变后触发的,且 change 后的值导致了超标),
@@ -1247,6 +1271,44 @@ layout("/layouts/platform_h5.html"){
return Number(option.selectNum) || 0 return Number(option.selectNum) || 0
}, },
/**
* 计算当前选项可达到的最大份数。
* 多选项目同时受项目剩余份数和单个套餐是否允许多份控制;单选项目固定为1。
* @param option 当前福利选项,selectNum为已选份数
* @return {number} 传给步进器max属性的最大份数
*/
getOptionMaxSelectNum(option) {
if (this.projectInfo.isCheckBox === "radio") {
return 1
}
const currentQuantity = Number(option.selectNum) || 0
const globalAllowedQuantity = currentQuantity + this.remainingSelectionCount
if (!this.projectInfo.singleOptionSupportMultipleSelection) {
return Math.min(1, globalAllowedQuantity)
}
return globalAllowedQuantity
},
/**
* 判断当前选项是否还能增加份数。
* 达到项目总上限或单个套餐已达到1份且不允许多份时禁用加号,但不影响减号取消。
* @param option 当前福利选项,selectNum为已选份数
* @return {boolean} true表示禁用当前步进器的加号
*/
isOptionIncreaseDisabled(option) {
if (this.isDeadlinePassed) {
return true
}
if (this.projectInfo.isCheckBox === "radio") {
return this.selectedRadioId === option.id
}
const currentQuantity = Number(option.selectNum) || 0
if (!this.projectInfo.singleOptionSupportMultipleSelection && currentQuantity >= 1) {
return true
}
return this.remainingSelectionCount <= 0
},
/** /**
* 统一处理单选和多选项目的数量变化,保持原有默认项及总数量限制。 * 统一处理单选和多选项目的数量变化,保持原有默认项及总数量限制。
* @param index 福利选项索引 * @param index 福利选项索引
@@ -1261,16 +1323,28 @@ layout("/layouts/platform_h5.html"){
return return
} }
if (option.isSystemDefault) return if (option.isSystemDefault) return
this.selectedRadioId = "" this.syncRadioOptionSelection("")
this.projectInfo.options.forEach((item) => {
item.selectNum = 0
})
return return
} }
this.$set(option, "selectNum", quantity) this.$set(option, "selectNum", quantity)
this.selectNumChange(index, quantity) this.selectNumChange(index, quantity)
}, },
/**
* 同步单选项目的选中项和全部步进器显示值。
* @param optionId 当前选中的福利选项 ID;传空字符串表示取消当前选择
* @return {void} 无返回值,未选项统一显示 0,选中项显示 1
*/
syncRadioOptionSelection(optionId) {
this.$set(this, "selectedRadioId", optionId || "")
if (!this.projectInfo.options) return
this.projectInfo.options.forEach((option) => {
this.$set(option, "selectNum", option.id === optionId ? 1 : 0)
// Vant Stepper 会保留内部显示值,切换单选项时更新 key 强制同步所有控件。
this.$set(option, "selectNumKey", (option.selectNumKey || 0) + 1)
})
},
// 保留原有首次选择方法,兼容其他调用入口。 // 保留原有首次选择方法,兼容其他调用入口。
selectCheckboxOption(index) { selectCheckboxOption(index) {
if (this.isDeadlinePassed) { if (this.isDeadlinePassed) {
@@ -1366,11 +1440,7 @@ layout("/layouts/platform_h5.html"){
if (this.projectInfo.options && this.userSelection.length > 0) { if (this.projectInfo.options && this.userSelection.length > 0) {
// 单选模式 // 单选模式
if (this.projectInfo.isCheckBox === "radio" && this.userSelection[0]) { if (this.projectInfo.isCheckBox === "radio" && this.userSelection[0]) {
this.selectedRadioId = this.userSelection[0].selectOptionId this.syncRadioOptionSelection(this.userSelection[0].selectOptionId)
const selectedOption = this.projectInfo.options.find((opt) => opt.id === this.selectedRadioId)
if (selectedOption) {
selectedOption.selectNum = 1
}
} }
// 多选模式 // 多选模式
else { else {
@@ -1620,22 +1690,11 @@ layout("/layouts/platform_h5.html"){
return return
} }
if (this.systemDefaultOption && this.systemDefaultOption.id !== optionId) { if (this.systemDefaultOption && this.systemDefaultOption.id !== optionId) {
this.selectedRadioId = this.systemDefaultOption.id this.syncRadioOptionSelection(this.systemDefaultOption.id)
this.$toast.fail("系统默认福利不可取消") this.$toast.fail("系统默认福利不可取消")
return return
} }
this.selectedRadioId = optionId this.syncRadioOptionSelection(optionId)
// 重置所有选项的selectNum
if (this.projectInfo.options) {
this.projectInfo.options.forEach((option) => {
if (option.id === optionId) {
option.selectNum = 1
} else {
option.selectNum = 0
}
})
}
}, },
// 默认项只在首次选择时自动带入,已有历史选择不自动覆盖。 // 默认项只在首次选择时自动带入,已有历史选择不自动覆盖。