9 Commits
54 changed files with 1952 additions and 574 deletions
+16 -1
View File
@@ -1,6 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="nutz" />
<option name="name" value="nutz" />
<option name="url" value="https://jfrog.nutz.cn/artifactory/libs-release" />
</remote-repository>
<remote-repository>
<option name="id" value="nutz-snapshots" />
<option name="name" value="nutz-snapshots" />
<option name="url" value="https://jfrog.nutz.cn/artifactory/snapshots" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
@@ -27,4 +42,4 @@
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
</component>
</project>
</project>
+1 -2
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="KubernetesApiProvider"><![CDATA[{}]]></component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
@@ -10,4 +9,4 @@
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
</project>
</project>
@@ -3,10 +3,6 @@ package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNode;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RoleConstant;
@@ -49,7 +45,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
;
@@ -60,8 +55,6 @@ import java.util.Map;
@At("/platform/sys/unit")
public class SysUnitController {
private static final Log log = Logs.get();
/** 浙江交通职业技术学院在 sys_unit 中的单位主键,作为单位树未指定 pid 时的默认根。 */
private static final String DEFAULT_UNIT_ROOT_ID = "4133012036";
@Inject
private SysUnitService sysUnitService;
@Inject
@@ -78,20 +71,17 @@ public class SysUnitController {
}
/**
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
* @param unitName 单位名称模糊查询条件,可为空
* @param unitId 上级单位ID,可为空;为空时保留原有单位范围
* @return Resultdata.list 为按生日祝福编号排序的单位,data.totalCount 为总条数
*/
@At("/pageData")
@Ok("json")
@SaCheckLogin
public Object pageData(PageForm pageForm, String unitName, String unitId) {
Cnd cnd = Cnd.NEW();
// cnd.and("parentId", "is not", null).andEX("unitLevel", "=", unitLevel).asc("unitLevel").asc("unitcode");
cnd.and("parentId", "is not", null);
cnd.andEX("parentId", "=", unitId);
// cnd.and("unitTypeCode", "=", "1");
cnd.asc("unitcode");
cnd.and(Cnd.likeEX("name", unitName));
Pagination<Sys_unit> listPage = sysUnitService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), Sys_unit.class, cnd);
return Result.success(listPage);
return Result.success(sysUnitService.pageUnits(pageForm, unitName, unitId));
}
@At("/userPageData")
@@ -297,43 +287,34 @@ public class SysUnitController {
}
}
/**
* @param pid 单位树根ID;为空时使用学校默认根节点
* @param req 当前请求上下文
* @return Resultdata 为含 id、parentId、name、unitTypeCode、children 的单位树列表
*/
@At("/tree")
@Ok("json")
@SaCheckLogin
public Object tree(@Param("pid") String pid, HttpServletRequest req) {
try {
String rootId = StrUtil.blankToDefault(pid, DEFAULT_UNIT_ROOT_ID);
String virtualRootId = "root";
Cnd cnd = Cnd.NEW();
cnd.asc("unitcode");
List<Sys_unit> list = sysUnitService.query(cnd);
List<TreeNode<String>> nodeList = CollUtil.newArrayList();
for (int i = 0; i < list.size(); i++) {
Sys_unit unit = list.get(i);
/*
* 单位根节点存在 id 与 parentId 都为 0 的自引用数据。
* 构建树时把当前查询根挂到虚拟根下,避免“中国地质大学”和 parentId=0 的学院被构造成同级。
*/
String parentId = rootId.equals(unit.getId()) ? virtualRootId : unit.getParentId();
nodeList.add(new TreeNode<>(unit.getId(), parentId, unit.getName(), i)
.setExtra(
Map.of(
"unitTypeCode", unit.getUnitTypeCode()
)
));
}
List<Tree<String>> treeList = TreeUtil.build(nodeList, virtualRootId);
return Result.success(treeList);
return Result.success(sysUnitService.getUnitTree(pid));
} catch (Exception e) {
log.error("单位树加载失败,路径=/platform/sys/unit/treepid=" + pid, e);
return Result.error();
}
}
/**
* @param unit 单位表单;name/unitcode 为名称和编码,aliasName 为最长100字简称,birthdaySortNo 为可空整数编号
* @param parentId 上级单位ID,新建时决定所属单位
* @param req 请求上下文;编辑时 birthdaySortNo 传空表示清空,未传表示保留
* @return Resultcode=0 表示保存成功,msg 为操作提示
*/
@At
@Ok("json")
@SaCheckPermission("sys.manager.unit.add")
@SLog(tag = "新建单位", msg = "单位名称:${args[0].name}")
@Aop(TransAop.READ_COMMITTED)
public Object addDo(@Param("..") Sys_unit unit, @Param("parentId") String parentId, HttpServletRequest req) {
try {
if ("root".equals(parentId)) {
@@ -343,6 +324,7 @@ public class SysUnitController {
sysUnitService.save(unit, parentId);
return Result.success();
} catch (Exception e) {
log.error("单位保存失败,接口=addDo,单位ID=" + unit.getId(), e);
return Result.error();
}
}
@@ -373,16 +355,24 @@ public class SysUnitController {
}
}
/**
* @param unit 单位表单;name/unitcode 为名称和编码,aliasName 为最长100字简称,birthdaySortNo 为可空整数编号
* @param parentId 上级单位ID,新建时决定所属单位
* @param req 请求上下文;编辑时 birthdaySortNo 传空表示清空,未传表示保留
* @return Resultcode=0 表示保存成功,msg 为操作提示
*/
@At
@Ok("json")
@SaCheckPermission("sys.manager.unit.edit")
@SLog(tag = "编辑单位", msg = "单位名称:${args[0].name}")
@Aop(TransAop.READ_COMMITTED)
public Object editDo(@Param("..") Sys_unit unit, @Param("parentId") String parentId, HttpServletRequest req) {
try {
unit.setUpdatedBy(SecurityUtil.getUserId());
sysUnitService.updateIgnoreNull(unit);
sysUnitService.updateUnit(unit, req.getParameter("birthdaySortNo") != null);
return Result.success();
} catch (Exception e) {
log.error("单位保存失败,接口=editDo,单位ID=" + unit.getId(), e);
return Result.error();
}
}
@@ -1,5 +1,7 @@
package com.budwk.app.sys.services;
import cn.hutool.core.lang.tree.Tree;
import java.util.List;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.BaseService;
@@ -9,6 +11,27 @@ import com.budwk.app.sys.models.Sys_unit;
* Created by wizzer on 2016/12/22.
*/
public interface SysUnitService extends BaseService<Sys_unit> {
/**
* 按生日祝福编号升序分页;空编号排后,同编号和空编号按单位编码升序。
* @param pageForm 分页参数,pageNumber 为页码,pageSize 为每页条数
* @param unitName 单位名称模糊查询条件,可为空
* @param unitId 上级单位ID,可为空;为空时查询 parentId 非空的单位
* @return 分页结果,list 为当前页单位,totalCount 为符合条件的单位总数
*/
Pagination<Sys_unit> pageUnits(PageForm pageForm, String unitName, String unitId);
/**
* @param pid 查询根单位ID;为空时使用学校默认根
* @return 单位树,节点包含 id、parentId、name、unitTypeCode 和 children;类别允许为空
*/
List<Tree<String>> getUnitTree(String pid);
/**
* @param unit 待编辑单位,id 必填;aliasName 为简称,birthdaySortNo 为可空整数
* @param birthdaySortNoProvided 请求是否提交排序编号;true 时允许将原编号清空,false 时保留原值
*/
void updateUnit(Sys_unit unit, boolean birthdaySortNoProvided);
/**
* 分页查询指定分工会的组成单位。
*
@@ -38,6 +38,7 @@ import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Base64;
@@ -270,10 +271,12 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
}
}
HtmlSaveOptions saveOptions = new HtmlSaveOptions();
// Aspose输出和字节流读取必须固定使用UTF-8,避免不同服务器的JVM默认编码导致中文乱码。
saveOptions.setEncoding(StandardCharsets.UTF_8);
saveOptions.setExportImagesAsBase64(true);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
doc.save(bos, saveOptions);
String htmlContent = Jsoup.parse(bos.toString()).body().html();
String htmlContent = Jsoup.parse(bos.toString(StandardCharsets.UTF_8)).body().html();
Pattern pattern = Pattern.compile(IMG_BASE64_PATTERN, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(htmlContent);
@@ -101,6 +101,11 @@ public class SysMsgServiceImpl extends BaseServiceImpl<Sys_msg> implements SysMs
}
});
// false 仅保存并通知站内消息,供业务自行按渠道发送,避免再次触发学校平台组合推送。
if (!isExternal) {
return sysMsg;
}
//发送学校平台消息
ThreadUtil.execute(() -> {
// 46表示短信和钉钉组合发送;按照消息中心要求,组合中包含短信时手机号必填。
@@ -1,6 +1,13 @@
package com.budwk.app.sys.services.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNode;
import cn.hutool.core.lang.tree.TreeUtil;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
@@ -25,6 +32,69 @@ import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
@IocBean(args = {"refer:dao"})
@CacheDefaults(cacheName = RedisConstant.PLATFORM_REDIS_WKCACHE_PREFIX + "sys_unit",isHash = true)
public class SysUnitServiceImpl extends BaseServiceImpl<Sys_unit> implements SysUnitService {
/** 学校单位主键,未指定 pid 时作为单位树的默认根。 */
private static final String DEFAULT_UNIT_ROOT_ID = "4133012036";
/**
* 单位列表与树共用数据库排序:有编号的优先(含0),编号升序后以单位编码升序兜底。
* @return 仅包含排序规则的新条件对象,可继续追加各自的查询条件
*/
private Cnd unitBirthdayOrder() {
Cnd cnd = Cnd.NEW();
cnd.asc("CASE WHEN birthdaySortNo IS NULL THEN 1 ELSE 0 END")
.asc("birthdaySortNo").asc("unitcode");
return cnd;
}
/** 保留单位名称与上级单位筛选条件,在数据库分页前完成统一排序。 */
@Override
public Pagination<Sys_unit> pageUnits(PageForm pageForm, String unitName, String unitId) {
Cnd cnd = unitBirthdayOrder();
cnd.and("parentId", "is not", null);
cnd.andEX("parentId", "=", unitId);
cnd.and(Cnd.likeEX("name", unitName));
return listPage(pageForm.getPageNumber(), pageForm.getPageSize(), Sys_unit.class, cnd);
}
/** 构建单位树时保留类别空值,避免历史数据因 Map.of 禁止 null 而导致整棵树加载失败。 */
@Override
public List<Tree<String>> getUnitTree(String pid) {
String rootId = StrUtil.blankToDefault(pid, DEFAULT_UNIT_ROOT_ID);
String virtualRootId = "root";
List<Sys_unit> list = query(unitBirthdayOrder());
List<TreeNode<String>> nodeList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
Sys_unit unit = list.get(i);
/*
* 单位根节点存在 id 与 parentId 都为 0 的自引用数据。
* 构建树时把当前查询根挂到虚拟根下,保证当前根节点与其下属单位的层级关系。
*/
String parentId = rootId.equals(unit.getId()) ? virtualRootId : unit.getParentId();
// 使用查询顺序作为权重,树构建后同级节点仍遵循生日祝福编号排序。
nodeList.add(new TreeNode<>(unit.getId(), parentId, unit.getName(), i)
.setExtra(
new HashMap<>(java.util.Collections.singletonMap("unitTypeCode", unit.getUnitTypeCode()))
));
}
List<Tree<String>> treeList = TreeUtil.build(nodeList, virtualRootId);
return treeList;
}
/** 编辑单位时保留原有非空更新规则,仅对明确提交的空排序编号执行清空。 */
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateUnit(Sys_unit unit, boolean birthdaySortNoProvided) {
if (unit == null || StrUtil.isBlank(unit.getId())) {
throw new BaseException("单位ID不能为空");
}
updateIgnoreNull(unit);
// 未提交字段的其他调用方不受影响;显式空值需要绕过 updateIgnoreNull。
if (birthdaySortNoProvided && unit.getBirthdaySortNo() == null) {
update(Chain.make("birthdaySortNo", null), Cnd.where("id", "=", unit.getId()));
}
}
public SysUnitServiceImpl(Dao dao) {
super(dao);
}
@@ -141,6 +141,10 @@ public class View_user {
@Column
private String unionCode;
/** 用户视图中的所属校区值,供变更表单回显及原值比较使用。 */
@Column
private String campus;
@Column
private String campusId;
@@ -2,6 +2,13 @@ package com.budwk.app.zhgh.staffmanage.birthday.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.annotation.RepeatSubmit;
import com.budwk.app.base.annotation.SLog;
import cn.hutool.core.util.StrUtil;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.mvc.annotation.POST;
import org.nutz.mvc.annotation.Param;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdayPageForm;
import com.budwk.app.zhgh.staffmanage.birthday.service.UserBirthdayService;
import io.swagger.annotations.Api;
@@ -23,6 +30,27 @@ import javax.servlet.http.HttpServletResponse;
@Api(tags = "生日祝福数据导出")
public class UserBirthdayExportController {
/**
* 修改生日列表中指定人员的所属校区,复用生日导出权限。
*
* @param userId 列表行的人员 ID;工号、姓名由服务端查询,不接收前端覆盖
* @param campusId 校区编码,仅允许 hz(杭州校区)或 cx(长兴校区)
* @return Resultcode=0 表示保存成功,msg 为操作提示,无额外业务数据
*/
@At
@POST
@RepeatSubmit
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("staff.birthday.export")
@SLog(tag = "生日校区维护", msg = "修改人员生日校区")
public Result saveCampus(@Param("userId") String userId, @Param("campusId") String campusId) {
if (StrUtil.isBlank(userId) || !StrUtil.equalsAny(campusId, "hz", "cx")) {
return Result.error("请选择人员和有效的所属校区");
}
userBirthdayService.saveCampus(userId, campusId);
return Result.success("所属校区修改成功");
}
@Inject
private UserBirthdayService userBirthdayService;
@@ -22,6 +22,15 @@ public interface UserBirthdayService extends BaseService<Sys_user> {
*/
Pagination pageData(UserBirthdayPageForm pageForm);
/**
* 按人员工号新增或更新生日校区标记,保留现有备注及创建信息。
*
* @param userId 当前操作者可管理的生日会员 ID
* @param campusId hz 表示杭州校区,cx 表示长兴校区
* @throws com.budwk.app.base.exception.BaseException 参数无效或人员不在可管理范围时抛出
*/
void saveCampus(String userId, String campusId);
/**
* 导出符合查询条件的生日会员。
*
@@ -4,13 +4,19 @@ 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.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.zhgh.staffmanage.birthday.models.UserBirthdayCampus;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_msg;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.sys.services.SysMsgService;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.zhgh.staffmanage.birthday.models.UserBirthdayConfig;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdayPageForm;
import com.budwk.app.zhgh.staffmanage.birthday.param.UserBirthdaySendMsgForm;
@@ -63,6 +69,9 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
@Inject
private SysMsgService sysMsgService;
@Inject
private SmsService smsService;
@Inject
private UserBirthdayMsgLogService userBirthdayMsgLogService;
@@ -75,6 +84,60 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), buildBirthdaySql(pageForm));
}
/**
* 校验人员范围并保存生日校区;锁定人员行,使同一人员的并发保存串行执行。
*
* @param userId 列表人员 ID,须属于当前操作者可查询的会员且已维护生日
* @param campusId hz(杭州校区)或 cx(长兴校区),名称由后端确定
* @throws BaseException 人员无效、超出管理范围或校区编码无效
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void saveCampus(String userId, String campusId) {
if (StrUtil.isBlank(userId) || !StrUtil.equalsAny(campusId, "hz", "cx")) {
throw new BaseException("请选择人员和有效的所属校区");
}
// 即使尚未创建校区标记,也通过已存在的人员行互斥,避免并发插入重复工号标记。
Sql lockSql = Sqls.create("SELECT id FROM sys_user WHERE id = @userId FOR UPDATE");
lockSql.params().set("userId", userId);
lockSql.setCallback(Sqls.callback.str());
dao().execute(lockSql);
if (lockSql.getObject(String.class) == null) {
throw new BaseException("人员不存在");
}
// 复用生日列表的组织权限条件,范围由登录身份确定,不信任客户端传入的组织信息。
Cnd scope = Cnd.where("id", "=", userId).and("member", "=", 1).and("birthday", "is not", null);
new UserBirthdayPageForm().buildSearch(scope, "");
View_user user = dao().fetch(View_user.class, scope);
if (user == null || StrUtil.isBlank(user.getLoginname())) {
throw new BaseException("人员不在可管理的生日会员范围内");
}
List<UserBirthdayCampus> records = dao().query(UserBirthdayCampus.class,
Cnd.where("loginname", "=", user.getLoginname()).desc("updatedAt").asc("id"));
UserBirthdayCampus campus = records.stream()
.filter(item -> !Boolean.TRUE.equals(item.getDelFlag()))
.findFirst().orElse(records.isEmpty() ? new UserBirthdayCampus() : records.get(0));
boolean insert = StrUtil.isBlank(campus.getId());
campus.setLoginname(user.getLoginname());
campus.setUsername(user.getUsername());
campus.setCampusId(campusId);
campus.setCampusName("cx".equals(campusId) ? "长兴校区" : "杭州校区");
campus.setDelFlag(false);
if (insert) {
dao().insert(campus);
} else {
dao().update(campus);
}
// 当前工号若已有重复有效记录,仅保留本次更新的记录,其余逻辑删除,避免列表联表重复。
for (UserBirthdayCampus duplicate : records) {
if (!duplicate.getId().equals(campus.getId()) && !Boolean.TRUE.equals(duplicate.getDelFlag())) {
duplicate.setDelFlag(true);
dao().update(duplicate);
}
}
}
@Override
public void exportXlsx(UserBirthdayPageForm pageForm, HttpServletResponse response) {
try {
@@ -95,7 +158,19 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
// 当前列表按查询校区生成顶部标题,未指定校区时使用全校标题。
exportParams.setTitle(buildSignatureTitle(getCampusDisplayName(pageForm.getCampusName())));
try (Workbook workbook = ExcelExportUtil.exportExcel(exportParams, columns, list)) {
// 调整当前工作簿的全部字体,保留各字体原有的加粗、颜色和字体名称。
for (int fontIndex = 0; fontIndex < workbook.getNumberOfFontsAsInt(); fontIndex++) {
workbook.getFontAt(fontIndex).setFontHeightInPoints((short) 16);
}
// 字号增大后为每行保留至少 28 磅高度,已有更高的标题或数据行不缩小。
for (Sheet sheet : workbook) {
for (Row row : sheet) {
row.setHeightInPoints(Math.max(row.getHeightInPoints(), 28));
}
}
CommonDownloadUtil.download("生日祝福人员名单.xlsx", workbook, response);
}
} catch (Exception e) {
@@ -433,9 +508,9 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
sheet.setColumnWidth(7, 16 * 256);
CellStyle titleStyle = createCellStyle(workbook, true, (short) 16, false);
CellStyle subtitleStyle = createCellStyle(workbook, false, (short) 12, false);
CellStyle headerStyle = createCellStyle(workbook, true, (short) 11, true);
CellStyle contentStyle = createCellStyle(workbook, false, (short) 11, true);
CellStyle subtitleStyle = createCellStyle(workbook, false, (short) 16, false);
CellStyle headerStyle = createCellStyle(workbook, true, (short) 16, true);
CellStyle contentStyle = createCellStyle(workbook, false, (short) 16, true);
Row titleRow = sheet.createRow(0);
titleRow.setHeightInPoints(28);
@@ -443,13 +518,13 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 7));
Row subtitleRow = sheet.createRow(1);
subtitleRow.setHeightInPoints(22);
subtitleRow.setHeightInPoints(28);
createCell(subtitleRow, 0, buildSignatureSubtitle(displayPeriod), subtitleStyle);
sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 7));
String[] headers = {"序号", "部门", "姓名", "签名", "序号", "部门", "姓名", "签名"};
Row headerRow = sheet.createRow(2);
headerRow.setHeightInPoints(22);
headerRow.setHeightInPoints(28);
for (int i = 0; i < headers.length; i++) {
createCell(headerRow, i, headers[i], headerStyle);
}
@@ -457,7 +532,7 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
int leftSize = (list.size() + 1) / 2;
for (int i = 0; i < leftSize; i++) {
Row row = sheet.createRow(i + 3);
row.setHeightInPoints(22);
row.setHeightInPoints(28);
fillSignatureRow(row, 0, i, getListItem(list, i), contentStyle);
fillSignatureRow(row, 4, i + leftSize, getListItem(list, i + leftSize), contentStyle);
}
@@ -551,10 +626,10 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
}
if (start.getYear() == end.getYear()) {
return start.format(DateTimeFormatter.ofPattern("yyyy年M月")) + "-" +
end.format(DateTimeFormatter.ofPattern("M月"));
end.format(DateTimeFormatter.ofPattern("M月"));
}
return start.format(DateTimeFormatter.ofPattern("yyyy年M月")) + "-" +
end.format(DateTimeFormatter.ofPattern("yyyy年M月"));
end.format(DateTimeFormatter.ofPattern("yyyy年M月"));
}
private String buildSignatureDisplayPeriod(LocalDate start, LocalDate end) {
@@ -563,10 +638,10 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
}
if (start.getYear() == end.getYear()) {
return start.format(DateTimeFormatter.ofPattern("yyyy年M")) + "-" +
end.format(DateTimeFormatter.ofPattern("M月"));
end.format(DateTimeFormatter.ofPattern("M月"));
}
return start.format(DateTimeFormatter.ofPattern("yyyy年M月")) + "-" +
end.format(DateTimeFormatter.ofPattern("yyyy年M月"));
end.format(DateTimeFormatter.ofPattern("yyyy年M月"));
}
private String buildSignatureDateDisplayPeriod(LocalDate start, LocalDate end) {
@@ -575,21 +650,22 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
}
if (isSameMonth(start, end)) {
return start.format(DateTimeFormatter.ofPattern("yyyy年M月d日")) + "-" +
end.format(DateTimeFormatter.ofPattern("d日"));
end.format(DateTimeFormatter.ofPattern("d日"));
}
if (start.getYear() == end.getYear()) {
return start.format(DateTimeFormatter.ofPattern("yyyy年M月d日")) + "-" +
end.format(DateTimeFormatter.ofPattern("M月d日"));
end.format(DateTimeFormatter.ofPattern("M月d日"));
}
return start.format(DateTimeFormatter.ofPattern("yyyy年M月d日")) + "-" +
end.format(DateTimeFormatter.ofPattern("yyyy年M月d日"));
end.format(DateTimeFormatter.ofPattern("yyyy年M月d日"));
}
/** 统一列表和签名表的顶部标题,校区取按钮参数或列表筛选条件。 */
private String buildSignatureTitle(String campusDisplayName) {
if (StrUtil.isBlank(campusDisplayName)) {
return "浙江交通职业技术学院职工生日蛋糕券发放名单";
return "浙江交通职业技术学院职工座谈会及生日蛋糕券发放名单";
}
return "浙江交通职业技术学院" + campusDisplayName + "职工生日蛋糕券发放名单";
return "浙江交通职业技术学院" + campusDisplayName + "职工座谈会及生日蛋糕券发放名单";
}
private String buildSignatureSubtitle(String displayPeriod) {
@@ -643,8 +719,9 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
/**
* 将生日消息写入当前项目消息中心并生成发送记录。
* 发送内容只包含标题和正文,不向站内、短信或钉钉渠道传递生日页面链接。
* 单人、批量和自动发送共用此入口:站内保留跳转地址,短信仅发祝福正文,钉钉正文附带生日链接。
*/
@Aop(TransAop.READ_COMMITTED)
private int sendMessages(List<String> loginNames, String title, String content,
String pushBy, String pushByName, String pushType) {
List<String> recipients = loginNames == null ? List.of() : loginNames.stream()
@@ -655,17 +732,50 @@ public class UserBirthdayServiceImpl extends BaseServiceImpl<Sys_user> implement
return 0;
}
// 链接携带发送时的福利贺卡文件 ID;未配置时仍进入生日页面,由页面展示未配置提示。
String fileId = StrUtil.blankToDefault(getConfig().getBirthdayUrl(), "");
String link = StrUtil.removeSuffix(Globals.AppDomain, "/")
+ "/platform/staffManage/birthday/manage/h5";
Sys_msg message = new Sys_msg();
message.setTitle(title);
message.setNote(content);
message.setUrl(link);
message.setType("user");
message.setSendType("show");
message.setSendAt(Times.getTS());
message.setCreatedBy(pushBy);
sysMsgService.saveMsg(message, recipients.toArray(String[]::new), true);
userBirthdayMsgLogService.insertLogs(recipients, title, content, "",
pushBy, pushByName, pushType);
// 短信接收人需携带工号和手机号,沿用消息中心按工号分组的查询范围。
Sql receiverSql = Sqls.create("SELECT loginname, mobile FROM vw_user $condition");
receiverSql.setCondition(Cnd.where("loginname", "in", recipients).groupBy("loginname"));
List<NutMap> receivers = listMap(receiverSql).stream()
.map(user -> NutMap.NEW()
.addv("userId", user.getString("loginname"))
.addv("mobile", user.getString("mobile"))
.addv("email", "")
.addv("flag", 0))
.toList();
if (!receivers.isEmpty()) {
// 分别提交渠道任务,任一渠道失败不会阻断另一渠道;实际结果由学校消息服务分别留存。
ThreadUtil.execute(() -> {
boolean success = smsService.sendMsg("4", null, receivers, title, content, null, null);
if (!success) {
log.warn("生日短信发送未成功,消息ID:{},请查看学校平台发送记录", message.getId());
}
});
ThreadUtil.execute(() -> {
// 当前钉钉使用文本消息,链接明确写入正文,跳转参数留空以避免平台重复追加。
boolean success = smsService.sendMsg("6", null, receivers, title, content, null, link);
if (!success) {
log.warn("生日钉钉发送未成功,消息ID:{},请查看学校平台发送记录", message.getId());
} else {
userBirthdayMsgLogService.insertLogs(recipients, title, content, link,
pushBy, pushByName, pushType);
}
});
}
return recipients.size();
}
}
@@ -87,6 +87,7 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
u.unitName,
u.unionId,
u.unionName,
u.campus,
u.birthday,
u.manyUnit,
u.member,
@@ -267,6 +268,7 @@ public class MemberManageServiceImpl extends BaseServiceImpl<Sys_user> implement
NutMap newMap = Lang.obj2nutmap(record);
// 原数据
NutMap sourceMap = Lang.obj2nutmap(info);
// 原数据已映射 campus,直接与表单同名字段比较,避免被 campusId 覆盖而误报异动。
List<NutMap> changeList = new ArrayList<>();
for (String fieldName : allowChangeFieldNames) {
@@ -165,6 +165,60 @@ public class WelfareListController {
welfareListService.exportXlsx(pageForm, response);
}
/**
* 导出指定福利项目和套餐的报销表。
*
* @param projectId 福利项目 ID
* @param optionId 福利套餐 ID,必须属于 projectId 对应项目
* @param response XLSX 文件下载响应
*/
@At
@Ok("void")
@SaCheckPermission("welfare.list.mange")
@ApiOperation("导出福利报销表")
public void doExcelByOptionId(@Param("projectId") String projectId,
@Param("optionId") String optionId,
HttpServletResponse response) {
welfareListService.doExcelByOptionId(projectId, optionId, response);
}
/**
* 导出供货商使用的套餐选择数据,按套餐拆分工作表。
*
* @param projectId 福利项目 ID
* @param unionId 可选的分工会 ID;为空时按当前用户的数据权限导出
* @param response XLSX 文件下载响应
*/
@At
@Ok("void")
@SaCheckPermission("welfare.list.mange")
@ApiOperation("导出供货商选择数据")
public void exportSelectData(@Param("projectId") String projectId,
@Param("unionId") String unionId,
HttpServletResponse response) {
welfareListService.exportSelectData(projectId, unionId, response);
}
/**
* 导入供应商提供的快递单号文件。
*
* @param file XLS 或 XLSX 文件,包含工号与最多四个快递单号列
* @param projectId 福利项目 ID
* @param optionId 福利套餐 ID,必须属于 projectId 对应项目
* @return 导入结果;成功时返回成功提示,失败时返回具体原因
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("welfare.list.mange")
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
@SLog(tag = "福利名单管理", msg = "导入福利快递单号")
@ApiOperation("导入福利快递单号")
public Result importCourierNumber(@Param("file") TempFile file,
@Param("projectId") String projectId,
@Param("optionId") String optionId) {
return welfareListService.importCourierNumber(file, projectId, optionId);
}
@At
@SaCheckPermission("welfare.list.mange")
@ApiOperation("编辑备注")
@@ -60,7 +60,6 @@ public class WelfareMineController {
END AS isChoose,
GROUP_CONCAT(DISTINCT wuso.optionName ,'',wus.selectNum,'份)') AS gist_list,
MAX(wus.selectTime) AS selectTime,
MAX(wus.deliveryDate) AS deliveryDate,
wus.receiveAddress,
wl.userId
FROM
@@ -98,7 +97,6 @@ public class WelfareMineController {
wpus.receiveAddress,
wpso.optionName,
wpus.selectNum,
wpus.deliveryDate,
wpus.remark,
wpus.userSign,
wpus.courierNumber,
@@ -0,0 +1,51 @@
package com.budwk.app.zhgh.welfare.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.welfare.service.WelfareProjectService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;
/**
* 福利通知附件查看入口。
*/
@IocBean
@Ok("json:full")
@At("/platform/welfare/notice")
@Api(tags = "福利通知")
public class WelfareNoticeController {
@Inject
private WelfareProjectService projectService;
@At
@Ok("beetl:/platform/zhghh5/welfare/notice/detail.html")
@ApiOperation("福利通知附件详情页")
@SaCheckLogin
public void index() {
}
/**
* 查询福利通知附件详情。
*
* @param id 福利项目 ID
* @return JSON 结果,data 中包含项目名称、文件名称、下载地址和 PDF 预览地址
*/
@At
@ApiOperation("查询福利通知附件详情")
@SaCheckLogin
public Result detailData(@Valid String id) {
if (StrUtil.isBlank(id)) {
return Result.error("福利项目ID不能为空");
}
return Result.success(projectService.getNoticeAttachmentInfo(id));
}
}
@@ -110,8 +110,8 @@ public class WelfareStatisticsController {
@Ok("void")
@SaCheckPermission("welfare.statistics")
@ApiOperation("按福利选项导出")
public void exportByWelfareOptions(String projectId, HttpServletResponse response) {
welfareStatisticsService.exportByWelfareOptions(projectId, response);
public void exportByWelfareOptions(String projectId, String unionId, HttpServletResponse response) {
welfareStatisticsService.exportByWelfareOptions(projectId, unionId, response);
}
@At
@@ -8,7 +8,6 @@ import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
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.service.WelfareProjectService;
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.Param;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@IocBean
@Ok("json:full")
@@ -57,7 +54,6 @@ public class WelfareUserSelectController {
ELSE 0
END AS isChoose,
wus.selectTime,
MAX(wus.deliveryDate) AS deliveryDate,
MAX(wus.remark) AS remark,
GROUP_CONCAT(DISTINCT wuso.optionName ,'',wus.selectNum,'份)') AS gist_list,
wus.receiveAddress
@@ -93,8 +89,6 @@ public class WelfareUserSelectController {
if (count == 0) {
return Result.error("您没有选择的权限");
}
WelfareProject project = dao.fetch(WelfareProject.class, projectId);
Date selectTime = new Date();
String additionalInfoValidationMessage = welfareProjectService.validateSelectionAdditionalInfo(selections, selectTime);
if (additionalInfoValidationMessage != null) {
@@ -112,9 +106,9 @@ public class WelfareUserSelectController {
return Result.error(groupQuantityValidationMessage);
}
int sum = Arrays.stream(selections).mapToInt(selection -> Objects.requireNonNullElse(selection.getSelectNum(), 0)).sum();
if (sum > project.getMultiSelectNum()) {
return Result.error("选择的数量不能超过" + project.getMultiSelectNum());
String projectQuantityValidationMessage = welfareProjectService.validateProjectSelectionQuantity(projectId, selections);
if (projectQuantityValidationMessage != null) {
return Result.error(projectQuantityValidationMessage);
}
@@ -30,9 +30,19 @@ public class WelfareExportEntityTc {
@Excel(name = "已选福利", width = 80d)
private String optionName;
@Excel(name = "收货信息", width = 100d)
// V4 原“收货信息”导出列保留字段,现按 V3 规则拆分为收货人、联系方式和收货地址。
// @Excel(name = "收货信息", width = 100d)
private String receiveAddress;
@Excel(name = "收货人", width = 15d)
private String recipient;
@Excel(name = "联系方式", width = 20d)
private String phone;
@Excel(name = "收货地址", width = 60d)
private String address;
@Excel(name = "快递单号", width = 40d)
private String courierNumber;
@@ -207,6 +207,14 @@ public class WelfareProject extends BaseModel implements SysHomeConvert {
@ColDefine(type = ColType.VARCHAR, width = 200)
private String cover;
/**
* 福利通知附件下载地址,只允许关联一个 Word 文件。
*/
@Column
@Comment("福利通知附件")
@ColDefine(type = ColType.VARCHAR, width = 200)
private String noticeAttachment;
@Column
@Comment("是否全年生日蛋糕卷")
@ColDefine(type = ColType.BOOLEAN)
@@ -37,4 +37,10 @@ public class WelfareSelectionSituationPageForm extends PageForm {
@ApiModelProperty("人员属性")
private String userAttribute;
@ApiModelProperty("人员类型")
private String[] personTypes;
@ApiModelProperty("在职状态")
private String[] userStates;
}
@@ -13,10 +13,32 @@ import javax.servlet.http.HttpServletResponse;
public interface WelfareListService extends BaseService<WelfareList> {
/**
* 导出一个福利项目下指定套餐的报销表。
*
* @param projectId 福利项目 ID
* @param optionId 福利套餐 ID,必须属于指定福利项目
* @param response XLSX 文件下载响应
*/
void doExcelByOptionId(String projectId, String optionId, HttpServletResponse response);
/**
* 导出供应商所需的选择数据,并按套餐创建工作表。
*
* @param projectId 福利项目 ID
* @param unionId 可选的分工会 ID;为空时按当前登录人的数据范围导出
* @param response XLSX 文件下载响应
*/
void exportSelectData(String projectId, String unionId, HttpServletResponse response);
/**
* 导入指定福利项目、套餐下的快递单号。
*
* @param file XLS 或 XLSX 文件,列为工号、姓名和最多四个快递单号
* @param projectId 福利项目 ID
* @param optionId 福利套餐 ID,必须属于指定福利项目
* @return 成功或失败的导入结果
*/
Result importCourierNumber(TempFile file, String projectId, String optionId);
void doEditWelfareData(String editWelfareData, String welfareOptions);
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.welfare.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
import com.budwk.app.zhgh.welfare.model.WelfareUserSelection;
import org.nutz.lang.util.NutMap;
import java.util.Date;
import java.util.List;
@@ -25,6 +26,14 @@ public interface WelfareProjectService extends BaseService<WelfareProject> {
*/
List<WelfareProject> listHistoryProject();
/**
* 查询福利通知详情页需要的项目和附件信息。
*
* @param projectId 福利项目 ID
* @return 包含 projectName、fileId、fileName、suffix、attachmentUrl 和 previewUrl 的详情数据;未上传附件时文件字段为空
*/
NutMap getNoticeAttachmentInfo(String projectId);
/**
* 校验福利选择是否保留系统默认福利。
*
@@ -44,11 +53,19 @@ public interface WelfareProjectService extends BaseService<WelfareProject> {
String validateGroupSelectionQuantity(String projectId, WelfareUserSelection[] selections);
/**
* 校验福利选择附加信息。配送时间为必填项,必须按日期粒度不早于本次选择时间;
* 同一次提交的全部福利项必须使用相同配送时间和备注。
* 校验福利项目一次提交的总选择份数。
*
* @param selections 用户提交的福利选择数组,每项需包含 deliveryDate,可选包含 remark
* @param selectTime 后端生成的本次选择时间,用于防止客户端伪造日期
* @param projectId 福利项目 ID,用于读取单选或多选模式及多选数量上限
* @param selections 用户提交的福利选择数组,每项需包含 selectOptionId 和 selectNum
* @return 校验通过返回 {@code null};超过项目上限或多选项目未配置上限时返回错误提示
*/
String validateProjectSelectionQuantity(String projectId, WelfareUserSelection[] selections);
/**
* 校验福利选择附加信息。同一次提交的全部福利项必须使用相同备注,配送时间字段已停用。
*
* @param selections 用户提交的福利选择数组,每项可包含 remark
* @param selectTime 后端生成的本次选择时间,保留该参数以兼容现有调用接口
* @return 校验通过返回 {@code null},否则返回可直接展示的错误提示
*/
String validateSelectionAdditionalInfo(WelfareUserSelection[] selections, Date selectTime);
@@ -15,12 +15,25 @@ public interface WelfareSelectionSituationService extends BaseService<WelfareLis
* 根据选择情况页面的全部查询条件发送钉钉消息。
*
* @param pageForm 查询条件,包含项目、福利选项、人员信息、组织范围及选择状态
* @param messageContent 纯文本消息内容,不应包含HTML标签或富文本样式
* @param messageContent 纯文本消息内容,不应包含HTML标签或富文本样式;发送前会补充福利通知详情地址
* @return 实际提交到钉钉消息渠道的去重人员数量
*/
int sendDingTalkMessage(WelfareSelectionSituationPageForm pageForm, String messageContent);
/**
* 按选择情况页面的查询条件导出人员选择明细。
*
* @param pageForm 页面查询条件,包含项目、福利选项、姓名、工号、组织范围、人员类型和选择状态
* @param response HTTP响应,返回包含收货备注、快递单号和签字图片的XLSX文件流
*/
void exportXlsx(WelfareSelectionSituationPageForm pageForm, HttpServletResponse response);
/**
* 导出福利领取表。
*
* @param pageForm 页面查询条件,projectId指定福利项目
* @param response HTTP响应,返回包含福利选项、收货备注和签字图片的XLSX文件流
*/
void receiveXlsx(WelfareSelectionSituationPageForm pageForm, HttpServletResponse response);
/**
@@ -62,7 +62,7 @@ public interface WelfareStatisticsService extends BaseService<WelfareProject> {
* @param projectId
* @param response
*/
void exportByWelfareOptions(String projectId, HttpServletResponse response);
void exportByWelfareOptions(String projectId, String unionId, HttpServletResponse response);
/**
* 导出汇总表
@@ -15,8 +15,8 @@ import javax.servlet.http.HttpServletResponse;
public interface WelfareUserEvaluationService extends BaseService<WelfareUserEvaluation> {
/**
* 校验当前用户是否已到福利配送日期。只有存在选择记录、全部记录已设置配送日期,且当前日期
* 不早于配送日期时才允许打开或提交评价
* 校验当前用户是否已经选择福利且项目活动已经结束。活动结束时间以福利项目的
* choiceTimeEnd 为准,PC、移动端与接口提交使用同一规则
*
* @param projectId 福利项目ID
* @param userId 当前登录用户ID
@@ -61,8 +61,17 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
@Override
public void doExcelByOptionId(String projectId, String optionId, HttpServletResponse response) {
if (StrUtil.isBlank(projectId) || StrUtil.isBlank(optionId)) {
log.warn("导出报销表失败:福利项目或套餐不能为空");
return;
}
WelfareProject project = dao().fetch(WelfareProject.class, projectId);
WelfareProjectSubjectOption subjectOption = dao().fetch(WelfareProjectSubjectOption.class, optionId);
WelfareProjectSubjectOption subjectOption = dao().fetch(WelfareProjectSubjectOption.class,
Cnd.where("id", "=", optionId).and("welfareId", "=", projectId));
if (project == null || subjectOption == null) {
log.warn("导出报销表失败:福利项目或套餐不存在,projectId={}, optionId={}", projectId, optionId);
return;
}
Sql sql = Sqls.create("""
SELECT
@@ -75,30 +84,35 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
wpus.selectNum
FROM
welfare_project_user_selection wpus
LEFT JOIN `welfare_list` wl ON wpus.selectUserId = wl.userId
INNER JOIN `welfare_list` wl ON wl.userId = wpus.selectUserId
AND wl.projectId = wpus.welfareId
LEFT JOIN `sys_user` u ON u.id = wl.userId
LEFT JOIN sys_union un ON un.id = wl.welfareUnitId
LEFT JOIN sys_unit it ON it.id = wl.welfareSecondLevelUnitId
LEFT JOIN sys_union un ON un.id = wl.welfareUnionId
LEFT JOIN sys_unit it ON it.id = wl.welfareUnitId
WHERE
wl.projectId = @welfareId
AND wpus.selectOptionId = @selectOptionId and wpus.selectNum!=0
wpus.welfareId = @welfareId
AND wpus.selectOptionId = @selectOptionId
AND IFNULL(wpus.selectNum, 0) <> 0
ORDER BY wl.welfareUnionName, wl.welfareUnitName, u.loginname
""").setParam("welfareId", projectId).setParam("selectOptionId", optionId);
List<NutMap> mapList = listMap(sql);
List<String> userId = mapList.stream().map(m -> m.getString("id")).collect(Collectors.toList());
List<WelfareCourierNumber> courierNumberList = dao().query(WelfareCourierNumber.class,
List<WelfareCourierNumber> courierNumberList = userId.isEmpty()
? Collections.emptyList()
: dao().query(WelfareCourierNumber.class,
Cnd.where("selectUserId", "in", userId)
.and("welfareId", "=", projectId)
.and("selectOptionId", "=", optionId));
.and("selectOptionId", "=", optionId)
.asc("createdAt"));
List<String> nameList = List.of("one", "two", "three", "four");
for (NutMap map : mapList) {
List<WelfareCourierNumber> userCourierNumbers = courierNumberList.stream()
.filter(c -> c.getSelectUserId().equals(map.getString("id"))).collect(Collectors.toList());
for (int i = 0; i < userCourierNumbers.size(); i++) {
for (int i = 0; i < userCourierNumbers.size() && i < nameList.size(); i++) {
map.setv(nameList.get(i) + "CourierNumber", userCourierNumbers.get(i).getCourierNumber());
}
}
@@ -138,14 +152,13 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
exportParams.setSheetName(un);
service.createSheetForMap(workbook, exportParams, exportEntities, v);
}*/
try {
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
exportParams.setTitle(project.getName() + subjectOption.getOptionName() + "发放名单");
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, mapList);
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
exportParams.setTitle(project.getName() + subjectOption.getOptionName() + "报销表");
try (Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, mapList)) {
CommonDownloadUtil.download(project.getName() + subjectOption.getOptionName() + ".xlsx", workbook, response);
} catch (Exception e) {
e.printStackTrace();
log.error("导出报销表失败,projectId={}, optionId={}", projectId, optionId, e);
}
}
@@ -153,6 +166,10 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
@Override
public void exportSelectData(String projectId, String unionId, HttpServletResponse response) {
WelfareProject welfareProject = dao().fetch(WelfareProject.class, Cnd.where("id", "=", projectId));
if (welfareProject == null) {
log.warn("导出供货商选择数据失败:福利项目不存在,projectId={}", projectId);
return;
}
String projectName = welfareProject.getName();
Cnd cnd = Cnd.NEW();
Sql sql = Sqls.create("""
@@ -170,16 +187,18 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
left join `sys_user` u on u.id = wpus.selectUserId
left join welfare_project_subject_option wpso on wpso.id = wpus.selectOptionId
left join welfare_list wl on wl.userId = wpus.selectUserId and wl.projectId=wpus.welfareId
LEFT JOIN sys_union un ON un.id = wl.welfareUnitId
LEFT JOIN sys_unit it ON it.id = wl.welfareSecondLevelUnitId
LEFT JOIN sys_union un ON un.id = wl.welfareUnionId
LEFT JOIN sys_unit it ON it.id = wl.welfareUnitId
$condition
""");
cnd.and("wpus.welfareId", "=", projectId);
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name(), RoleConstant.SCHOOL_UNION_WELFARE_ADMIN.name())) {
cnd.and("wl.welfareUnitId", "=", SecurityUtil.getUnionId());
if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_SPORTS.name())) {
cnd.and("wl.welfareUnionId", "=", SecurityUtil.getUnionId());
}
cnd.andEX("wl.welfareUnitId", "=", unionId);
cnd.andEX("wl.welfareUnionId", "=", unionId);
cnd.groupBy("wpus.selectUserId", "wpus.selectOptionId");
cnd.asc("it.unitcode");
sql.setCondition(cnd);
@@ -207,12 +226,21 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
try {
CommonDownloadUtil.download(projectName + ".xlsx", workbook, response);
} catch (Exception e) {
e.printStackTrace();
log.error("导出供货商选择数据失败,projectId={}", projectId, e);
}
}
@Override
@Aop(TransAop.READ_COMMITTED)
public Result importCourierNumber(TempFile file, String projectId, String optionId) {
if (StrUtil.isBlank(projectId) || StrUtil.isBlank(optionId)) {
return Result.error("福利项目和套餐不能为空");
}
WelfareProjectSubjectOption subjectOption = dao().fetch(WelfareProjectSubjectOption.class,
Cnd.where("id", "=", optionId).and("welfareId", "=", projectId));
if (subjectOption == null) {
return Result.error("福利套餐不存在或不属于当前福利项目");
}
if (Lang.isEmpty(file)) {
return Result.error("上传的文件不能为空");
}
@@ -226,7 +254,7 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
ImportParams importParams = new ImportParams();
excelList = ExcelImportUtil.importExcel(file.getFile(), CourierNumberExcelMode.class, importParams);
} catch (Exception e) {
e.printStackTrace();
log.warn("读取快递单号导入文件失败", e);
return Result.error("读取不到数据,请检查excel文件格式");
}
@@ -236,7 +264,7 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
try {
if (excelList.stream().anyMatch(v -> StrUtil.isBlank(v.getLoginName()))) {
return Result.error("工号和快递单号不能为空");
return Result.error("工号不能为空");
}
List<CourierNumberExcelMode> filterExcelList = excelList.stream().collect(Collectors.collectingAndThen(
@@ -249,6 +277,9 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
Sql loginNameSql = Sqls.create("select id,loginname from sys_user where loginname in (@loginNameList)");
loginNameSql.setParam("loginNameList", loginNameList);
List<NutMap> loginNameMaps = listMap(loginNameSql);
if (loginNameMaps.isEmpty()) {
return Result.error("Excel中的工号均未匹配到系统用户");
}
List<String> userIds = loginNameMaps.stream().map(v -> v.getString("id")).collect(Collectors.toList());
@@ -291,12 +322,13 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
welfareCourierNumbers.add(courierNumber);
}
}
insert(welfareCourierNumbers);
if (!welfareCourierNumbers.isEmpty()) {
insert(welfareCourierNumbers);
}
return Result.success("导入成功");
} catch (Exception e) {
e.printStackTrace();
log.error("导入快递单号失败,projectId={}, optionId={}", projectId, optionId, e);
return Result.error("导入快递单号失败");
}
@@ -685,12 +717,10 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
t2.loginname,
t2.username,
t2.sex,
DATE_FORMAT(t2.birthday, '%Y-%m-%d') AS birthday,
threeUnit.name AS threeUnitName
DATE_FORMAT(t2.birthday, '%Y-%m-%d') AS birthday
FROM
`welfare_list` t1
LEFT JOIN sys_user t2 ON t2.id = t1.userId
LEFT JOIN sys_unit threeUnit ON threeUnit.id = t2.threeUnitId
$condition
""");
Cnd cnd = Cnd.NEW();
@@ -735,7 +765,6 @@ public class WelfareListServiceImpl extends BaseServiceImpl<WelfareList> impleme
entities.add(new ExcelExportEntity("在职状态", "userState", 20));
entities.add(new ExcelExportEntity("所属工会", "welfareUnionName", 20));
entities.add(new ExcelExportEntity("所属单位", "welfareUnitName", 20));
entities.add(new ExcelExportEntity("三级单位", "threeUnitName", 20));
entities.add(new ExcelExportEntity("备注", "remark", 20));
// 设置导出参数
@@ -4,6 +4,7 @@ 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_file;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.welfare.model.WelfareList;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
@@ -79,6 +80,25 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
return dao().query(WelfareProject.class, Cnd.NEW().desc(WelfareProject::getYear).desc(WelfareProject::getChoiceTimeStart));
}
@Override
public NutMap getNoticeAttachmentInfo(String projectId) {
WelfareProject project = fetch(projectId);
if (project == null) {
throw new IllegalArgumentException("福利项目不存在");
}
NutMap result = NutMap.NEW().addv("projectName", project.getName());
if (StrUtil.isBlank(project.getNoticeAttachment())) {
return result;
}
Sys_file noticeFile = resolveNoticeAttachmentFile(project.getNoticeAttachment());
return result
.addv("fileId", noticeFile.getId())
.addv("fileName", noticeFile.getName())
.addv("suffix", noticeFile.getSuffix())
.addv("attachmentUrl", noticeFile.getDownloadPath())
.addv("previewUrl", "/platform/sys/file/convertPDF?id=" + noticeFile.getId());
}
@Override
public String validateSystemDefaultOptionSelection(String projectId, WelfareUserSelection[] selections) {
List<WelfareProjectSubjectOption> systemDefaultOptions = dao().query(WelfareProjectSubjectOption.class,
@@ -182,8 +202,42 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
}
/**
* 校验一次选择提交中的配送日期和备注。日期比较统一截断到当天零点,允许选择当天配送;
* 多项福利的附加信息必须一致,避免同一次选择产生互相冲突的评价开放时间
* 校验福利项目总选择份数。单选项目固定只能选择 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;
}
/**
* 校验一次选择提交中的备注。配送时间字段已停用,新提交数据统一清空该字段;
* 多项福利的备注必须一致,避免同一次选择出现互相冲突的附加信息。
*
* @param selections 当前提交的福利选择数据
* @param selectTime 后端生成的选择时间
@@ -194,30 +248,23 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
if (selections == null || selections.length == 0) {
return "请至少选择一项福利";
}
Date selectionDate = DateUtil.beginOfDay(selectTime == null ? new Date() : selectTime);
Date submittedDeliveryDate = null;
String submittedRemark = null;
boolean firstSelection = true;
for (WelfareUserSelection selection : selections) {
if (selection == null || selection.getDeliveryDate() == null) {
return "请选择配送时间";
}
Date deliveryDate = DateUtil.beginOfDay(selection.getDeliveryDate());
if (deliveryDate.before(selectionDate)) {
return "配送时间不能早于选择时间";
}
if (submittedDeliveryDate == null) {
submittedDeliveryDate = deliveryDate;
submittedRemark = StrUtil.trim(selection.getRemark());
} else if (!submittedDeliveryDate.equals(deliveryDate)) {
return "同一次选择的配送时间必须一致";
} else if (!Objects.equals(submittedRemark, StrUtil.trim(selection.getRemark()))) {
return "同一次选择的备注必须一致";
if (selection == null) {
return "福利选择数据不能为空";
}
String remark = StrUtil.trim(selection.getRemark());
if (firstSelection) {
submittedRemark = remark;
firstSelection = false;
} else if (!Objects.equals(submittedRemark, remark)) {
return "同一次选择的备注必须一致";
}
if (remark != null && remark.length() > 200) {
return "备注不能超过200字";
}
selection.setDeliveryDate(deliveryDate);
selection.setDeliveryDate(null);
selection.setRemark(remark);
}
return null;
@@ -257,6 +304,7 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
@Override
@Aop(TransAop.READ_COMMITTED)
public void saveProject(WelfareProject project) {
normalizeNoticeAttachment(project);
// 保存前根据福利选项分组去重并校验选择数量范围,防止非法 JSON 配置进入数据库。
normalizeGroupSelectionConfigs(project);
// 插入项目
@@ -272,6 +320,7 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateProject(WelfareProject project) {
normalizeNoticeAttachment(project);
// 修改项目时同样归一化分组配置,确保新增、编辑两条保存链路规则一致。
normalizeGroupSelectionConfigs(project);
// 更新项目
@@ -325,6 +374,55 @@ public class WelfareProjectServiceImpl extends BaseServiceImpl<WelfareProject> i
dao().clear(Sys_home_activity.class, Cnd.where("id", "=", id));
}
/**
* 校验福利通知附件数量和真实文件类型,并将提交地址归一化为系统文件下载地址。
*
* @param project 待保存的福利项目,noticeAttachment 应为单个系统文件下载地址
*/
private void normalizeNoticeAttachment(WelfareProject project) {
String attachment = StrUtil.trim(project.getNoticeAttachment());
if (StrUtil.isBlank(attachment)) {
project.setNoticeAttachment(null);
return;
}
if (attachment.contains(",")) {
throw new IllegalArgumentException("福利通知附件只能上传一个文件");
}
Sys_file noticeFile = resolveNoticeAttachmentFile(attachment);
String suffix = StrUtil.blankToDefault(noticeFile.getSuffix(), "").toLowerCase(Locale.ROOT);
if (!"doc".equals(suffix) && !"docx".equals(suffix)) {
throw new IllegalArgumentException("福利通知附件只能上传Word文件");
}
project.setNoticeAttachment(noticeFile.getDownloadPath());
}
/**
* 从系统下载地址解析文件 ID,并确认文件记录真实存在。
*
* @param attachment 系统文件下载地址,格式为 /platform/sys/file/download?id=文件ID
* @return 对应的系统文件记录
*/
private Sys_file resolveNoticeAttachmentFile(String attachment) {
int idIndex = attachment.indexOf("id=");
if (idIndex < 0) {
throw new IllegalArgumentException("福利通知附件地址无效");
}
String fileId = attachment.substring(idIndex + 3);
int parameterIndex = fileId.indexOf('&');
if (parameterIndex >= 0) {
fileId = fileId.substring(0, parameterIndex);
}
fileId = fileId.trim();
if (StrUtil.isBlank(fileId)) {
throw new IllegalArgumentException("福利通知附件地址无效");
}
Sys_file noticeFile = dao().fetch(Sys_file.class, fileId);
if (noticeFile == null) {
throw new IllegalArgumentException("福利通知附件文件不存在,请重新上传");
}
return noticeFile;
}
/**
* 根据福利选项中的非空分组生成最终配置数组,并校验最小、最大选择数量。
* 最大选择数量为 0 时表示不限,不参与最小值与最大值的大小比较。
@@ -14,6 +14,7 @@ import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.sms.SmsService;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.sys.models.Sys_file;
@@ -21,6 +22,7 @@ import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.welfare.model.WelfareCourierNumber;
import com.budwk.app.zhgh.welfare.model.WelfareList;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
import com.budwk.app.zhgh.welfare.model.WelfareProjectSubjectOption;
@@ -47,6 +49,7 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
@@ -75,7 +78,7 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
GROUP_CONCAT(DISTINCT t3.optionName ,'',t2.selectNum,'份)') AS selectedOptions,
GROUP_CONCAT(DISTINCT t2.mobile) AS mobile,
GROUP_CONCAT(DISTINCT t2.receiveAddress) AS receiveAddress,
MAX(t2.deliveryDate) AS deliveryDate,
MAX(t2.remark) AS remark,
t4.username AS userName,
t4.loginname AS loginName,
t4.sex,
@@ -143,13 +146,36 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
String title = project.getName() + "-福利通知";
boolean success = smsService.sendMsg("6", loginNames, null, title, plainContent, null, null);
String detailUrl = StrUtil.removeSuffix(Globals.AppDomain, "/")
+ "/platform/welfare/notice/index?id=" + project.getId();
String detailLine = "查看详情:" + detailUrl;
String messageBody = removeExistingNoticeDetailLinks(plainContent, detailUrl);
String sendContent = StrUtil.isBlank(messageBody) ? detailLine : messageBody + "\n" + detailLine;
// 详情地址已经包含在消息正文中,跳转参数保持为空,避免消息中心再次追加 PC、移动端详情链接。
boolean success = smsService.sendMsg("6", loginNames, null, title, sendContent, null, null);
if (!success) {
throw new BaseException("钉钉消息发送失败,请检查消息发送配置或稍后重试");
}
return loginNames.size();
}
/**
* 删除正文中已存在的当前福利详情链接,避免管理员在默认链接前继续输入文字后重复追加。
*
* @param content 管理员在发送弹窗中编辑的纯文本正文,可在任意位置包含“查看详情:URL”
* @param detailUrl 当前福利项目的标准详情地址
* @return 已移除全部当前项目详情链接并清理空白行的消息正文;没有其他正文时返回空字符串
*/
private String removeExistingNoticeDetailLinks(String content, String detailUrl) {
String detailPattern = "查看详情\\s*[:]\\s*" + Pattern.quote(detailUrl);
String contentWithoutDetail = content.replaceAll(detailPattern, "");
return Arrays.stream(contentWithoutDetail.split("\\R"))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.collect(Collectors.joining("\n"))
.trim();
}
/**
* 构建选择情况列表与消息接收人共用的筛选条件。
* 除页面条件外会主动叠加当前登录人的分工会数据范围。
@@ -169,6 +195,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
cnd.andEX("t2.selectOptionId", "=", pageForm.getWelfareOptionId());
cnd.andEX("t4.aidFundMemberUserType", "=", pageForm.getAidFundMemberUserType());
cnd.andEX("t4.userAttribute", "=", pageForm.getUserAttribute());
cnd.andEX("t1.personType", "in", pageForm.getPersonTypes());
cnd.andEX("t1.userState", "in", pageForm.getUserStates());
if (StrUtil.isNotBlank(pageForm.getUserName())) {
cnd.where().andLike("t4.username", pageForm.getUserName());
}
@@ -188,12 +216,16 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
Sql sql = Sqls.create("""
SELECT
t1.id,
t1.userId,
t1.welfareUnionName,
t1.welfareUnitName,
GROUP_CONCAT(DISTINCT t3.optionName,'',t2.selectNum,'份)') AS selectedOptions,
GROUP_CONCAT(DISTINCT t2.mobile) AS mobile,
GROUP_CONCAT(DISTINCT t2.userName) AS userName2,
GROUP_CONCAT(DISTINCT t2.receiveAddress) AS receiveAddress,
GROUP_CONCAT(DISTINCT t2.courierNumber) AS legacyCourierNumber,
MAX(t2.userSign) AS userSign,
MAX(t2.remark) AS remark,
t4.username AS userName,
t4.loginname AS loginName,
t4.sex
@@ -221,6 +253,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
cnd.andEX("t1.welfareUnitId", "in", pageForm.getUnitIds());
cnd.andEX("t1.welfareUnionId", "=", pageForm.getUnionId());
cnd.andEX("t1.personType", "in", pageForm.getPersonTypes());
cnd.andEX("t1.userState", "in", pageForm.getUserStates());
if (pageForm.getIsSelect() != null) {
if (pageForm.getIsSelect()) {
@@ -240,6 +274,8 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
cnd.groupBy("t1.id");
sql.setCondition(cnd);
List<NutMap> list = listMap(sql);
fillExportCourierNumbers(list, pageForm);
list.forEach(this::fillExportUserSign);
// 项目
WelfareProject project = dao().fetch(WelfareProject.class, pageForm.getProjectId());
@@ -259,6 +295,14 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
if(project.getProvideMode() == 3){
entities.add(new ExcelExportEntity("收货地址", "receiveAddress", 40));
}
entities.add(new ExcelExportEntity("备注", "remark", 30));
ExcelExportEntity courierNumberEntity = new ExcelExportEntity("快递单号", "courierNumber", 30);
courierNumberEntity.setWrap(true);
entities.add(courierNumberEntity);
ExcelExportEntity userSignEntity = new ExcelExportEntity("签字信息", "userSignBytes", 20);
userSignEntity.setType(2);
userSignEntity.setExportImageType(2);
entities.add(userSignEntity);
// 设置导出参数
ExportParams exportParams = new ExportParams();
@@ -271,6 +315,73 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
}
/**
* 批量补充选择情况导出的快递单号。
* 当前数据从快递单号表读取;历史数据仍兼容选择记录中的旧快递单号字段。
*
* @param rows 选择情况导出行,每行必须包含userId,处理后增加courierNumber文本字段
* @param pageForm 页面查询条件;welfareOptionId不为空时只导出所选套餐对应的快递单号
*/
private void fillExportCourierNumbers(List<NutMap> rows, WelfareSelectionSituationPageForm pageForm) {
List<String> userIds = rows.stream()
.map(row -> row.getString("userId"))
.filter(StrUtil::isNotBlank)
.distinct()
.toList();
if (userIds.isEmpty()) {
return;
}
Cnd courierCondition = Cnd.where(WelfareCourierNumber::getWelfareId, "=", pageForm.getProjectId())
.and(WelfareCourierNumber::getSelectUserId, "in", userIds)
.andEX(WelfareCourierNumber::getSelectOptionId, "=", pageForm.getWelfareOptionId());
courierCondition.asc(WelfareCourierNumber::getCreatedAt);
Map<String, List<WelfareCourierNumber>> courierNumbersByUser = dao()
.query(WelfareCourierNumber.class, courierCondition)
.stream()
.collect(Collectors.groupingBy(WelfareCourierNumber::getSelectUserId));
for (NutMap row : rows) {
LinkedHashSet<String> courierNumbers = courierNumbersByUser
.getOrDefault(row.getString("userId"), Collections.emptyList())
.stream()
.map(WelfareCourierNumber::getCourierNumber)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
String legacyCourierNumber = row.getString("legacyCourierNumber");
if (StrUtil.isNotBlank(legacyCourierNumber)) {
Arrays.stream(legacyCourierNumber.split(","))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.forEach(courierNumbers::add);
}
row.put("courierNumber", String.join("", courierNumbers));
}
}
/**
* 将签字文件路径转换为选择情况Excel可识别的图片字节。
* 文件记录缺失或文件读取失败时保留空白签字列,避免单个历史文件影响整份导出。
*
* @param row 选择情况导出行,输入userSign文件路径,输出userSignBytes图片字节
*/
private void fillExportUserSign(NutMap row) {
String userSignPath = row.getString("userSign");
if (StrUtil.isBlank(userSignPath)) {
return;
}
try {
Sys_file file = dao().fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", userSignPath));
if (file == null) {
log.warn("选择情况导出未找到签字文件记录,userId={}, userSign={}", row.getString("userId"), userSignPath);
return;
}
row.put("userSignBytes", SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath()));
} catch (Exception e) {
log.warn("选择情况导出签字文件读取失败,userId={}", row.getString("userId"), e);
}
}
@Override
public void receiveXlsx(WelfareSelectionSituationPageForm pageForm, HttpServletResponse response) {
@@ -278,6 +389,7 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
SELECT
t1.*,
t2.userSign,
MAX(t2.remark) AS remark,
t3.username userName,
t3.loginname loginName,
GROUP_CONCAT(DISTINCT t2.selectOptionId) AS selectOptionIds
@@ -308,26 +420,29 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
}
}
}
if (map.get("userSign") != null){
if (StrUtil.isNotBlank(map.getString("userSign"))){
String userSignPath = map.getString("userSign");
try {
Sys_file file = dao().fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", map.getString("userSign")));
Sys_file file = dao().fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", userSignPath));
if (file == null) {
// 签字路径没有对应文件记录时保留人员数据,仅将签字单元格导出为空。
map.put("userSign", null);
log.warn("领取表导出未找到签字文件记录,userId={}, userSign={}", map.getString("userId"), userSignPath);
continue;
}
byte[] userSignBytes = SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath());
map.put("userSign", userSignBytes);
}catch (Exception e){
e.printStackTrace();
log.error("下载图片失败", e);
// 签字文件读取异常不能阻断整张领取表导出,当前人员签字留空。
map.put("userSign", null);
log.warn("领取表导出签字文件读取失败,userId={}", map.getString("userId"), e);
}
}
}
List<Map<String, Object>> safeList = list.stream().map(nutMap -> {
Map<String, Object> map = new HashMap<>(nutMap);
return map;
}).toList();
// 分组
Map<String, List<Map<String, Object>>> listMap = safeList.stream()
.collect(Collectors.groupingBy(n -> (String) n.get("welfareUnionName")));
// 保留 NutMap 数据结构,避免 EasyPOI 在 Java 17 下反射普通 HashMap 导致工作表创建失败。
Map<String, List<NutMap>> listMap = list.stream()
.collect(Collectors.groupingBy(n -> n.getString("welfareUnionName")));
// 构建 Excel 列
List<ExcelExportEntity> entities = new ArrayList<>();
@@ -336,22 +451,23 @@ public class WelfareSelectionSituationServiceImpl extends BaseServiceImpl<Welfar
optionList.forEach(option -> {
entities.add(new ExcelExportEntity(option.getOptionName(), option.getOptionName(), 20));
});
entities.add(new ExcelExportEntity("备注", "remark", 30));
ExcelExportEntity userSignEntity = new ExcelExportEntity("签字", "userSign", 20);
userSignEntity.setType(2);
userSignEntity.setExportImageType(2);
entities.add(userSignEntity);
// 导出
Workbook workbook = new HSSFWorkbook();
Workbook workbook = new XSSFWorkbook();
listMap.forEach((k, v) -> {
ExcelExportService service = new ExcelExportService();
ExportParams exportParams = new ExportParams();
exportParams.setSheetName(k);
exportParams.setType(ExcelType.HSSF);
exportParams.setType(ExcelType.XSSF);
service.createSheetForMap(workbook, exportParams, entities, v);
});
CommonDownloadUtil.download("领取表.xls", workbook, response);
CommonDownloadUtil.download("领取表.xlsx", workbook, response);
}
/**
@@ -4,11 +4,14 @@ 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.afterturn.easypoi.excel.export.ExcelExportService;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.utils.CommonDownloadUtil;
import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.utils.SysFileMinIoUtil;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.welfare.mode.WelfareExportEntityTc;
@@ -31,9 +34,7 @@ import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
/**
@@ -159,9 +160,10 @@ public class WelfareSingleServiceImpl extends BaseServiceImpl implements Welfare
u.unitname AS unitName,
u.unionname AS unionName,
u.mobile,
IF(LOCATE('undefined',wpus.receiveAddress)>0,NULL,wpus.receiveAddress) as receiveAddress,
GROUP_CONCAT(DISTINCT wpso.optionName ,'',wpus.selectNum,'份)') optionName,
wpus.courierNumber
IF(LOCATE('undefined',wpus.receiveAddress)>0,NULL,wpus.receiveAddress) as receiveAddress,
GROUP_CONCAT(DISTINCT wpso.optionName ,'',wpus.selectNum,'份)') optionName,
wpus.courierNumber,
MAX(wpus.userSign) userSign
FROM
welfare_project_user_selection wpus
LEFT JOIN `vw_user` u ON u.id = wpus.selectUserId
@@ -183,6 +185,8 @@ public class WelfareSingleServiceImpl extends BaseServiceImpl implements Welfare
@Override
public Workbook exportReceiveDetail(String projectId, String unionId) {
// V4 原横向套餐签领表逻辑保留,当前按 V3 的“每个套餐一个工作表”格式执行。
if (false) {
Sql optionSql = Sqls.create("""
SELECT w2.id, w2.optionName
@@ -282,9 +286,148 @@ public class WelfareSingleServiceImpl extends BaseServiceImpl implements Welfare
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, dataList);
return workbook;
}
return exportReceiveDetailV3(projectId, unionId);
}
/**
* 生成参考项目格式的分工会福利签领表:一个工作表展示人员、套餐份数和签字,并在末行汇总套餐份数。
*
* @param projectId 福利项目ID
* @param unionId 分工会ID
* @return XLSX工作簿
*/
private Workbook exportReceiveDetailV3(String projectId, String unionId) {
Sql optionSql = Sqls.create("""
SELECT id, optionName
FROM welfare_project_subject_option
WHERE welfareId = @projectId
ORDER BY optionSort, id
""");
optionSql.setParam("projectId", projectId);
List<NutMap> options = listMap(optionSql);
Sql sql = Sqls.create("""
SELECT
wl.userId,
u.username AS userName,
u.loginname AS loginName,
wl.welfareUnitName AS unitName,
wpus.userSign,
wpus.selectOptionId,
wpus.selectNum
FROM welfare_list wl
LEFT JOIN sys_user u ON u.id = wl.userId
LEFT JOIN welfare_project_user_selection wpus ON wpus.selectUserId = wl.userId
AND wpus.welfareId = wl.projectId
WHERE wl.projectId = @projectId AND wl.welfareUnionId = @unionId
ORDER BY wl.welfareUnitName, u.loginname, wpus.selectOptionId
""");
sql.setParam("projectId", projectId);
sql.setParam("unionId", unionId);
List<NutMap> rows = listMap(sql);
rows.forEach(this::fillUserSign);
Map<String, List<NutMap>> userRows = rows.stream().collect(Collectors.groupingBy(
row -> row.getString("userId"), LinkedHashMap::new, Collectors.toList()));
List<NutMap> exportRows = new ArrayList<>();
int index = 1;
for (List<NutMap> selectedRows : userRows.values()) {
NutMap userRow = selectedRows.get(0);
NutMap exportRow = NutMap.NEW();
exportRow.put("no", index++);
exportRow.put("userName", userRow.getString("userName"));
exportRow.put("loginName", userRow.getString("loginName"));
exportRow.put("unitName", userRow.getString("unitName"));
exportRow.put("qzBytes", selectedRows.stream()
.map(row -> row.get("qzBytes"))
.filter(Objects::nonNull)
.findFirst()
.orElse(null));
for (NutMap option : options) {
int selectNum = selectedRows.stream()
.filter(row -> option.getString("id").equals(row.getString("selectOptionId")))
.mapToInt(row -> row.getInt("selectNum", 0))
.sum();
exportRow.put(option.getString("id"), selectNum);
}
exportRows.add(exportRow);
}
NutMap totalRow = NutMap.NEW();
totalRow.put("no", "合计");
for (NutMap option : options) {
int total = rows.stream()
.filter(row -> option.getString("id").equals(row.getString("selectOptionId")))
.mapToInt(row -> row.getInt("selectNum", 0))
.sum();
totalRow.put(option.getString("id"), total);
}
exportRows.add(totalRow);
List<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("序号", "no", 10));
entities.add(new ExcelExportEntity("姓名", "userName", 15));
entities.add(new ExcelExportEntity("工号", "loginName", 18));
ExcelExportEntity unitEntity = new ExcelExportEntity("所在单位", "unitName", 25);
unitEntity.setWrap(true);
entities.add(unitEntity);
for (NutMap option : options) {
entities.add(new ExcelExportEntity(option.getString("optionName"), option.getString("id"), 12));
}
ExcelExportEntity signEntity = new ExcelExportEntity("签字", "qzBytes", 20);
signEntity.setType(2);
signEntity.setExportImageType(2);
entities.add(signEntity);
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
return ExcelExportUtil.exportExcel(exportParams, entities, exportRows);
}
/** 从 V4 文件存储读取签名图片,历史文件缺失时保留空白签字格。 */
private void fillUserSign(NutMap row) {
String userSign = row.getString("userSign");
if (StrUtil.isBlank(userSign)) {
return;
}
try {
Sys_file file = dao().fetch(Sys_file.class, Cnd.where(Sys_file::getDownloadPath, "=", userSign));
if (file != null) {
row.put("qzBytes", SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath()));
}
} catch (Exception ignored) {
// 单个签名文件异常不能影响整份签领表导出。
}
}
/** 身份证仅显示前段信息,保持 V3 签领表的脱敏规则。 */
private String maskIdCard(String idCard) {
if (StrUtil.isBlank(idCard) || idCard.length() <= 4) {
return idCard;
}
return idCard.substring(0, idCard.length() - 4) + "****";
}
/** 将历史收货信息拆为 V3 名单所需的收货人、联系方式和收货地址字段。 */
private void fillV3ReceiveAddress(NutMap row) {
String receiveAddress = row.getString("receiveAddress");
if (StrUtil.isBlank(receiveAddress)) {
return;
}
Map<String, String> values = new HashMap<>();
for (String pair : receiveAddress.split("[,]")) {
String[] keyValue = pair.split("[:]", 2);
if (keyValue.length == 2) {
values.put(keyValue[0].trim(), keyValue[1].trim());
}
}
row.put("recipient", values.get("收件人"));
row.put("phone", values.get("联系方式"));
row.put("address", values.get("收货地址"));
}
@Override
public Object unclaimedData(String projectId, String unionId, Integer pageNumber, Integer pageSize) {
@@ -333,6 +476,7 @@ public class WelfareSingleServiceImpl extends BaseServiceImpl implements Welfare
}
List<NutMap> list2 = list.stream().filter(v -> Strings.isNotBlank(v.getString("unionName"))).collect(Collectors.toList());
list2.forEach(this::fillV3ReceiveAddress);
List<WelfareExportEntityTc> welfareExportEntityTcList = list2.stream()
.map(map -> JSONUtil.toBean(Json.toJson(map), WelfareExportEntityTc.class))
@@ -340,15 +484,15 @@ public class WelfareSingleServiceImpl extends BaseServiceImpl implements Welfare
welfareExportEntityTcList.forEach(v -> {
if (StrUtil.isNotBlank(v.getUserSign())) {
ByteArrayOutputStream signOs = new ByteArrayOutputStream();
// ftpService.download(v.getUserSign(), signOs);
byte[] imageBytes = signOs.toByteArray();
if (Lang.isNotEmpty(imageBytes)) {
v.setQzBytes(imageBytes);
} else {
v.setQzBytes(null);
try {
Sys_file file = dao().fetch(Sys_file.class,
Cnd.where(Sys_file::getDownloadPath, "=", v.getUserSign()));
if (file != null) {
v.setQzBytes(SysFileMinIoUtil.getFileBytes(file.getBucket(), file.getStoragePath()));
}
} catch (Exception ignored) {
// 签名文件缺失时保留空白,不中断名单导出。
}
}
});
ExportParams exportParams = new ExportParams();
@@ -17,6 +17,7 @@ import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.welfare.model.WelfareProject;
import com.budwk.app.zhgh.welfare.model.WelfareProjectSubjectOption;
import com.budwk.app.zhgh.welfare.service.WelfareSingleService;
import com.budwk.app.zhgh.welfare.service.WelfareStatisticsService;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
@@ -32,14 +33,15 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
@Slf4j
public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject> implements WelfareStatisticsService {
@org.nutz.ioc.loader.annotation.Inject
private WelfareSingleService welfareSingleService;
public WelfareStatisticsServiceImpl(Dao dao) {
super(dao);
}
@@ -65,7 +67,7 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
}};
// 选项数据
List<WelfareProjectSubjectOption> welfareOptions = dao().query(WelfareProjectSubjectOption.class, Cnd.where(WelfareProjectSubjectOption::getWelfareId, "=", projectId));
List<WelfareProjectSubjectOption> welfareOptions = getNaturalSortedOptions(projectId);
for (WelfareProjectSubjectOption welfareOption : welfareOptions) {
dynamicTableColumns.add(NutMap.NEW().addv("label", welfareOption.getOptionName()).addv("prop", welfareOption.getId()));
@@ -176,6 +178,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
SqlExpressionGroup seg = new SqlExpressionGroup();
seg.orLike("u.loginname", pageForm.getSearchKeyword());
seg.orLike("u.username", pageForm.getSearchKeyword());
// 已选人员弹窗仅支持按工号或姓名筛选,需将 OR 条件加入查询条件后才会生效。
cnd.and(seg);
}
sql.setCondition(cnd);
Pagination pagination = listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
@@ -184,6 +188,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
@Override
public void exportSelectedUnionUser(String projectId, String unionId, HttpServletResponse response) {
// V4 原已选人员导出逻辑保留,当前按 V3 字段与格式执行。
if (false) {
Sql sql = Sqls.create("""
SELECT
u.loginname AS loginName,
@@ -245,6 +251,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
} catch (Exception e) {
log.error("导出Excel失败", e);
}
}
welfareSingleService.exportReceiveDetailByUnionId(projectId, unionId, true, response);
}
@Override
@@ -289,6 +297,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
@Override
public void exportUnSelectedUnionUser(String projectId, String unionId, HttpServletResponse response) {
// V4 原未选人员导出逻辑保留,当前按 V3 字段与格式执行。
if (false) {
Sql sql = Sqls.create("""
SELECT
u.loginname AS loginName,
@@ -344,6 +354,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
} catch (Exception e) {
log.error("导出Excel失败", e);
}
}
welfareSingleService.exportReceiveDetailByUnionId(projectId, unionId, false, response);
}
@@ -377,7 +389,9 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
@Override
public void exportByWelfareOptions(String projectId, HttpServletResponse response) {
public void exportByWelfareOptions(String projectId, String unionId, HttpServletResponse response) {
// V4 原按福利选项导出逻辑保留,当前按 V3 的品牌分表字段执行。
if (false) {
Workbook workbook = new XSSFWorkbook();
List<ExcelExportEntity> exportEntities = new ArrayList<>();
@@ -434,6 +448,8 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
} catch (Exception e) {
log.error("导出Excel失败", e);
}
}
exportByWelfareOptionsV3(projectId, unionId, response);
}
@@ -456,7 +472,7 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
entities.add(unSelectedNumEntity);
// 选项数据
List<WelfareProjectSubjectOption> welfareOptions = dao().query(WelfareProjectSubjectOption.class, Cnd.where(WelfareProjectSubjectOption::getWelfareId, "=", projectId));
List<WelfareProjectSubjectOption> welfareOptions = getNaturalSortedOptions(projectId);
for (WelfareProjectSubjectOption welfareOption : welfareOptions) {
ExcelExportEntity entity = new ExcelExportEntity(welfareOption.getOptionName(), welfareOption.getId(), 20);
entity.setType(10);
@@ -543,4 +559,172 @@ public class WelfareStatisticsServiceImpl extends BaseServiceImpl<WelfareProject
}
}
/**
* 按 V3 品牌导出结构生成工作簿:每个福利选项独立工作表,人员多选时在每个对应选项表中均保留一行。
*
* @param projectId 福利项目ID
* @param unionId 当前筛选的分工会ID;为空时导出当前登录人权限范围内的全部分工会
* @param response HTTP响应,返回XLSX格式的品牌分表文件
*/
private void exportByWelfareOptionsV3(String projectId, String unionId, HttpServletResponse response) {
List<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("工号", "loginName", 20));
entities.add(new ExcelExportEntity("姓名", "userName", 20));
entities.add(new ExcelExportEntity("电话号码", "mobile", 20));
entities.add(new ExcelExportEntity("在职状态", "userState", 20));
entities.add(new ExcelExportEntity("人员类型", "personType", 20));
entities.add(new ExcelExportEntity("所在分工会", "welfareUnionName", 30));
entities.add(new ExcelExportEntity("所在单位", "welfareUnitName", 50));
entities.add(new ExcelExportEntity("选择份数", "selectNum", 10));
entities.add(new ExcelExportEntity("收货人", "recipient", 15));
entities.add(new ExcelExportEntity("联系方式", "phone", 20));
entities.add(new ExcelExportEntity("收货地址", "address", 60));
Sql sql = Sqls.create("""
SELECT
u.loginname AS loginName,
u.username AS userName,
wpus.mobile,
wpus.userName AS recipient,
wl.userState,
wl.personType,
wl.welfareUnionName,
wl.welfareUnitName,
wpus.receiveAddress,
wpus.selectNum,
wpus.selectOptionId
FROM welfare_project_user_selection wpus
LEFT JOIN welfare_list wl ON wl.userId = wpus.selectUserId AND wl.projectId = wpus.welfareId
LEFT JOIN sys_user u ON u.id = wpus.selectUserId
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("wpus.welfareId", "=", projectId);
cnd.and("wpus.selectNum", "!=", 0);
cnd.andEX("wl.welfareUnionId", "=", resolveExportUnionId(unionId));
cnd.asc("wl.welfareUnionName").asc("wl.welfareUnitName").asc("u.loginname");
sql.setCondition(cnd);
List<NutMap> rows = listMap(sql);
rows.forEach(this::fillV3ReceiveAddress);
try (Workbook workbook = new XSSFWorkbook()) {
for (WelfareProjectSubjectOption option : getNaturalSortedOptions(projectId)) {
List<NutMap> optionRows = rows.stream()
.filter(row -> option.getId().equals(row.getString("selectOptionId")))
// EasyPOI 生成工作表时会移除已处理行,必须传入可修改列表。
.collect(Collectors.toCollection(ArrayList::new));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
exportParams.setTitle(option.getOptionName() + "选择情况");
exportParams.setSheetName(option.getOptionName());
new ExcelExportService().createSheetForMap(workbook, exportParams, entities, optionRows);
}
CommonDownloadUtil.download("按品牌导出选择情况表.xlsx", workbook, response);
} catch (Exception e) {
log.error("按品牌导出Excel失败", e);
}
}
/** 分工会角色只能导出本人工会数据,校级角色可按页面筛选导出。 */
private String resolveExportUnionId(String unionId) {
if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name())
|| AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_SPORTS.name())) {
return SecurityUtil.getUnionId();
}
return unionId;
}
/**
* 填充品牌导出的收货信息。当前选择记录分别保存收货人、手机号和纯地址文本;
* 历史记录可能将三项信息拼接在 receiveAddress 中,因此继续兼容旧格式解析。
*/
private void fillV3ReceiveAddress(NutMap row) {
String mobile = row.getString("mobile");
if (StrUtil.isNotBlank(mobile)) {
row.put("phone", mobile);
}
String receiveAddress = row.getString("receiveAddress");
if (StrUtil.isBlank(receiveAddress)) {
return;
}
// 新数据的 receiveAddress 仅保存地址正文,可直接作为收货地址导出。
row.put("address", receiveAddress);
Map<String, String> values = new HashMap<>();
for (String pair : receiveAddress.split("[,]")) {
String[] keyValue = pair.split("[:]", 2);
if (keyValue.length == 2) {
values.put(keyValue[0].trim(), keyValue[1].trim());
}
}
String recipient = StrUtil.blankToDefault(values.get("收件人"), values.get("收货人"));
String phone = StrUtil.blankToDefault(values.get("联系方式"),
StrUtil.blankToDefault(values.get("手机号码"), values.get("手机号")));
String address = StrUtil.blankToDefault(values.get("收货地址"), values.get("详细地址"));
if (StrUtil.isNotBlank(recipient)) {
row.put("recipient", recipient);
}
if (StrUtil.isNotBlank(phone)) {
row.put("phone", phone);
}
if (StrUtil.isNotBlank(address)) {
row.put("address", address);
}
}
/**
* 按套餐名称进行自然排序,数字片段按数值比较、英文字母忽略大小写比较,避免“套餐10”排在“套餐2”之前。
*/
private List<WelfareProjectSubjectOption> getNaturalSortedOptions(String projectId) {
List<WelfareProjectSubjectOption> options = dao().query(WelfareProjectSubjectOption.class,
Cnd.where(WelfareProjectSubjectOption::getWelfareId, "=", projectId));
options.sort(Comparator.comparing(WelfareProjectSubjectOption::getOptionName, this::compareOptionNames)
.thenComparing(option -> option.getOptionSort() == null ? Integer.MAX_VALUE : option.getOptionSort())
.thenComparing(WelfareProjectSubjectOption::getId));
return options;
}
private int compareOptionNames(String left, String right) {
String leftName = StrUtil.blankToDefault(left, "");
String rightName = StrUtil.blankToDefault(right, "");
int leftIndex = 0;
int rightIndex = 0;
while (leftIndex < leftName.length() && rightIndex < rightName.length()) {
char leftChar = leftName.charAt(leftIndex);
char rightChar = rightName.charAt(rightIndex);
if (Character.isDigit(leftChar) && Character.isDigit(rightChar)) {
int leftEnd = leftIndex;
int rightEnd = rightIndex;
while (leftEnd < leftName.length() && Character.isDigit(leftName.charAt(leftEnd))) {
leftEnd++;
}
while (rightEnd < rightName.length() && Character.isDigit(rightName.charAt(rightEnd))) {
rightEnd++;
}
String leftNumber = leftName.substring(leftIndex, leftEnd).replaceFirst("^0+(?!$)", "");
String rightNumber = rightName.substring(rightIndex, rightEnd).replaceFirst("^0+(?!$)", "");
int numberCompare = Integer.compare(leftNumber.length(), rightNumber.length());
if (numberCompare != 0) {
return numberCompare;
}
numberCompare = leftNumber.compareTo(rightNumber);
if (numberCompare != 0) {
return numberCompare;
}
leftIndex = leftEnd;
rightIndex = rightEnd;
continue;
}
int charCompare = String.valueOf(leftChar).compareToIgnoreCase(String.valueOf(rightChar));
if (charCompare != 0) {
return charCompare;
}
leftIndex++;
rightIndex++;
}
return Integer.compare(leftName.length(), rightName.length());
}
}
@@ -4,7 +4,6 @@ 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.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
@@ -59,8 +58,8 @@ public class WelfareUserEvaluationServiceImpl extends BaseServiceImpl<WelfareUse
}
/**
* 评价开放时间以后端福利选择记录为准,避免客户端隐藏按钮后仍可直接调用评价接口。
* 配送时间按日期粒度比较,到达配送日期当天即可评价。
* 评价开放时间以后端福利项目活动结束时间为准,避免客户端隐藏按钮后仍可直接调用评价接口。
* 当前用户必须已经存在福利选择记录,且当前时间达到 choiceTimeEnd 才允许评价。
*
* @param projectId 福利项目ID
* @param userId 当前登录用户ID
@@ -77,14 +76,15 @@ public class WelfareUserEvaluationServiceImpl extends BaseServiceImpl<WelfareUse
if (selections.isEmpty()) {
return "当前项目暂无可评价的福利选择";
}
Date currentDate = DateUtil.beginOfDay(new Date());
for (WelfareUserSelection selection : selections) {
if (selection.getDeliveryDate() == null) {
return "当前福利尚未设置配送时间,暂不能评价";
}
if (currentDate.before(DateUtil.beginOfDay(selection.getDeliveryDate()))) {
return "配送时间未到,暂不能评价";
}
WelfareProject project = dao().fetch(WelfareProject.class, projectId);
if (project == null) {
return "福利项目不存在";
}
if (project.getChoiceTimeEnd() == null) {
return "当前福利未设置活动结束时间,暂不能评价";
}
if (new Date().before(project.getChoiceTimeEnd())) {
return "福利活动尚未结束,暂不能评价";
}
return null;
}
@@ -0,0 +1,123 @@
-- 补充旧版疗休养已完成移植、但未写入新版菜单表的两个 PC 端入口。
-- 使用 permission 做幂等判断,脚本可重复执行。
INSERT INTO sys_menu (
id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt,
delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService
)
SELECT
'8a691f4623db4d82927668b05d10b401',
parent_menu.id,
CONCAT(
parent_menu.path,
LPAD(
IFNULL((
SELECT MAX(CAST(RIGHT(child.path, 4) AS UNSIGNED))
FROM sys_menu child
WHERE child.parentId = parent_menu.id
), 0) + 1,
4,
'0'
)
),
'线路成团结果通知',
'Line Group Result Notice',
'menu',
'/platform/recuperation/lineStatistics',
'data-pjax',
'',
1,
0,
'recuperation.lineStatistics',
'查询线路报名统计并发送成团或未成团通知',
IFNULL((
SELECT MAX(child.location)
FROM sys_menu child
WHERE child.parentId = parent_menu.id
), parent_menu.location) + 1,
0,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
0,
'PC',
parent_menu.moduleId,
NULL,
'x',
0,
0
FROM sys_menu parent_menu
WHERE parent_menu.permission = 'recuperation'
AND NOT EXISTS (
SELECT 1
FROM (SELECT id FROM sys_menu WHERE permission = 'recuperation.lineStatistics') existing_menu
);
INSERT INTO sys_menu (
id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled,
permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt,
delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService
)
SELECT
'3074a944a1554982835793d1c164183f',
parent_menu.id,
CONCAT(
parent_menu.path,
LPAD(
IFNULL((
SELECT MAX(CAST(RIGHT(child.path, 4) AS UNSIGNED))
FROM sys_menu child
WHERE child.parentId = parent_menu.id
), 0) + 1,
4,
'0'
)
),
'省内灵活组团查询',
'Flexible Group Query',
'menu',
'/platform/recuperation/flexibleGroupQuery',
'data-pjax',
'',
1,
0,
'recuperation.flexibleGroupQuery',
'查询省内灵活组团、团队及报名人员',
IFNULL((
SELECT MAX(child.location)
FROM sys_menu child
WHERE child.parentId = parent_menu.id
), parent_menu.location) + 1,
0,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
'',
UNIX_TIMESTAMP(NOW()) * 1000,
0,
'PC',
parent_menu.moduleId,
NULL,
's',
0,
0
FROM sys_menu parent_menu
WHERE parent_menu.permission = 'recuperation'
AND NOT EXISTS (
SELECT 1
FROM (SELECT id FROM sys_menu WHERE permission = 'recuperation.flexibleGroupQuery') existing_menu
);
-- 系统管理员默认拥有补充菜单权限,其他业务角色可在角色管理中按需分配。
INSERT INTO sys_role_menu (roleId, menuId)
SELECT role_info.id, menu_info.id
FROM sys_role role_info
JOIN sys_menu menu_info ON menu_info.permission IN (
'recuperation.lineStatistics',
'recuperation.flexibleGroupQuery'
)
LEFT JOIN sys_role_menu role_menu
ON role_menu.roleId = role_info.id
AND role_menu.menuId = menu_info.id
WHERE role_info.code = 'SYSADMIN'
AND role_menu.roleId IS NULL;
+89 -9
View File
@@ -1,17 +1,85 @@
<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="false" scanPeriod="60000" debug="false">
<!-- 定义日志文件的存储路径 -->
<!-- 日志存储路径 -->
<property name="LOG_HOME" value="./logs"/>
<!-- 控制台输出配置 -->
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- INFO级别日志文件配置 -->
<!--
#################################################################################
# #
# 全文输出日志(一个文件存储)start #
# #
#################################################################################
-->
<!-- 所有级别日志写入同一个文件 -->
<appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/app.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/app-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
<!-- 框架日志级别(按需调整) -->
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="org.quartz" level="INFO"/>
<logger name="org.nutz" level="DEBUG"/>
<!-- root loggerDEBUG 级别,输出到控制台和全量文件 -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ALL_FILE"/>
</root>
<!--
#################################################################################
# #
# 全文输出日志(一个文件存储)end #
# #
#################################################################################
-->
<!--
#################################################################################
# #
# debug、info、error分文件输出 start #
# #
#################################################################################
-->
<!-- DEBUG级别日志文件配置 -->
<!--<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/debug.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/debug-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
&lt;!&ndash; INFO级别日志文件配置 &ndash;&gt;
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/info.log</file>
<encoder>
@@ -24,11 +92,11 @@
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
<!-- ERROR级别日志文件配置 -->
&lt;!&ndash; ERROR级别日志文件配置 &ndash;&gt;
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file>
<encoder>
@@ -41,19 +109,31 @@
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<maxHistory>300</maxHistory>
</rollingPolicy>
</appender>
<logger name="java" additivity="false" />
&lt;!&ndash; 框架日志级别 &ndash;&gt;
&lt;!&ndash; <logger name="java" additivity="false" />&ndash;&gt;
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="org.quartz" level="INFO"/>
<logger name="org.nutz" level="DEBUG"/>
<!-- 日志级别和appender的关联 -->
&lt;!&ndash; 日志级别和appender的关联 &ndash;&gt;
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</root>-->
<!--
#################################################################################
# #
# debug、info、error分文件输出 end #
# #
#################################################################################
-->
</configuration>
@@ -21,8 +21,10 @@
<button v-if="signatureContent" @click="openSignature" class="signature-reset-button"
type="button">重新签名</button>
</div>
<van-popup v-model="showSignaturePanel" :close-on-click-overlay="false"
:style="{ height: '100vh', width: '100vw' }" @close="handleSignaturePopupClose">
<!-- 签字板挂载到 body避免 iOS fixed 子元素裁切在外层带 transform 的滚动 Popup -->
<van-popup v-model="showSignaturePanel" class="signature-full-popup" get-container="body"
:close-on-click-overlay="false" :style="{ height: '100%', width: '100%' }"
@close="handleSignaturePopupClose">
<signature @save="save"></signature>
</van-popup>
</div>
@@ -1,6 +1,6 @@
<template>
<div class="signature-wrap">
<canvas id="canvas"></canvas>
<canvas ref="signatureCanvas"></canvas>
<div :class="isLikelyMobile() ? 'action-buttons' : 'action-buttons pc-action-buttons'">
<button @click="clear" class="action-button-danger" type="button">清空</button>
<button @click="undo" class="action-button-warning" type="button">撤销</button>
@@ -80,15 +80,13 @@ module.exports = {
}
},
mounted() {
const innerWidth = window.innerWidth
const innerHeight = window.innerHeight
const canvas = document.getElementById("canvas")
canvas.width = window.innerWidth
canvas.height = window.innerHeight
// 使用签字弹层的实际可视尺寸,避免 iOS WebView 中 100vh/innerHeight 包含不可见浏览器区域。
const containerWidth = this.$el.clientWidth || window.innerWidth
const containerHeight = this.$el.clientHeight || window.innerHeight
const canvas = this.$refs.signatureCanvas
signature = new SmoothSignature(canvas, {
width: innerWidth - 30,
height: innerHeight - 30,
width: Math.max(containerWidth - 30, 1),
height: Math.max(containerHeight - 30, 1),
scale: 2,
minWidth: 4,
maxWidth: 10,
@@ -101,11 +99,14 @@ module.exports = {
<style scoped>
.signature-wrap {
position: fixed;
left: 0;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 15px;
overflow: hidden;
box-sizing: border-box;
background: #ffffff;
}
@@ -121,8 +122,9 @@ module.exports = {
transform: rotate(90deg);
display: flex;
column-gap: 10px;
position: fixed;
bottom: 0px;
position: absolute;
z-index: 2;
bottom: calc(15px + env(safe-area-inset-bottom));
left: 60px;
transform: rotate(90deg);
display: flex;
@@ -621,7 +621,7 @@
</script>
<script nonce="${cspNonce!}">
const h5ComponentVersion = "20260821_2"
const h5ComponentVersion = "20260907_1"
Vue.component("rich-text", httpVueLoader("/components/plugins/sysRichTextView/index.vue?v=" + h5ComponentVersion))
Vue.component("h5-file-upload", httpVueLoader("/components/plugins/sysUpload/h5Index.vue?v=" + h5ComponentVersion))
Vue.component("h5-signature", httpVueLoader("/components/plugins/sysSignature/h5Index.vue?v=" + h5ComponentVersion))
@@ -16,6 +16,8 @@ const UNIT_MANAGE_TEMPLATE = {
<template v-slot="scope">{{scope.$index + (pageForm.pageNumber - 1) * pageForm.pageSize + 1}}</template>
</el-table-column>
<el-table-column label="单位名称" prop="name"></el-table-column>
<el-table-column label="简称" prop="aliasName" min-width="120"></el-table-column>
<el-table-column label="生日祝福排序编号" prop="birthdaySortNo" width="160"></el-table-column>
<el-table-column label="单位代码" prop="unitcode" width="200"></el-table-column>
<el-table-column label="单位等级" prop="unitLevel" width="100"></el-table-column>
<el-table-column label="操作" width="200px">
@@ -29,16 +31,21 @@ const UNIT_MANAGE_TEMPLATE = {
</el-card>
<el-dialog title="设置单位" :visible.sync="dialogVisible" :close-on-click-modal="false" width="40%" top="2%">
<el-form :model="formData" ref="form" :rules="formRules" size="small" label-width="80px">
<el-form :model="formData" ref="form" :rules="formRules" size="small" label-width="150px">
<el-form-item label="上级单位">
<el-input maxlength="100" disabled :value="currentData?.name" type="text"></el-input>
</el-form-item>
<el-form-item prop="name" label="单位名称">
<el-input maxlength="100" placeholder="请输入单位名称" v-model="formData.name" auto-complete="off" tabindex="2" type="text"></el-input>
</el-form-item>
<!-- <el-form-item prop="aliasName" label="单位别名">-->
<!-- <el-input maxlength="100" placeholder="请输入单位别名" v-model="formData.aliasName" auto-complete="off" tabindex="3" type="text"></el-input>-->
<!-- </el-form-item>-->
<!-- 简称与生日祝福排序编号复用单位实体字段;排序编号允许留空,控件与其他表单项等宽对齐。 -->
<el-form-item prop="aliasName" label="简称">
<el-input maxlength="100" placeholder="请输入简称" v-model="formData.aliasName" clearable></el-input>
</el-form-item>
<el-form-item prop="birthdaySortNo" label="生日祝福排序编号">
<el-input-number v-model="formData.birthdaySortNo" :precision="0" :step="1" style="width: 100%"
:min="-2147483648" :max="2147483647" placeholder="请输入排序编号"></el-input-number>
</el-form-item>
<el-form-item prop="unitcode" label="单位编码">
<el-input maxlength="100" placeholder="请输入单位编码" v-model="formData.unitcode" auto-complete="off" tabindex="4" type="text"></el-input>
</el-form-item>
@@ -75,10 +82,11 @@ const UNIT_MANAGE_TEMPLATE = {
<!-- </el-form-item>-->
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="doHandle">确 定</el-button>
<el-button @click="closeDialog" :disabled="formLoading">取 消</el-button>
<el-button type="primary" @click="doHandle" :loading="formLoading">确 定</el-button>
</span>
</el-dialog>
<slot></slot>
</div>
`,
props: {
@@ -111,6 +119,7 @@ const UNIT_MANAGE_TEMPLATE = {
},
tableData: [],
dialogVisible: false,
formLoading: false,
formData: {},
formRules: {
name: [{ required: true, message: "必填", trigger: "blur" }],
@@ -129,51 +138,71 @@ const UNIT_MANAGE_TEMPLATE = {
},
computed: {},
methods: {
// 通过组件方法关闭弹框,确保 $set 使用当前组件实例。
closeDialog() {
this.$set(this, "dialogVisible", false)
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
openAdd() {
this.dialogVisible = true
this.formData = {} //打开新增窗口,表单先清空
// 新建时清空扩展字段,避免沿用上一次编辑的值。
this.$set(this, "formData", { aliasName: "", birthdaySortNo: undefined })
this.parentUnit = []
this.options = []
},
doHandle() {
if (this.formLoading) return
this.$confirm("确定要提交吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
const self = this
const url = this.formData.id ? "/platform/sys/unit/editDo" : "/platform/sys/unit/addDo"
this.$refs["form"].validate(async function (valid) {
if (valid) {
if (self.currentNode) {
self.formData.parentId = self.currentData.id
}
const resp = await self.$axios.post(url, self.formData)
if (resp.code === 0) {
self.$message.success(resp.msg)
self.doSearch()
self.dialogVisible = false
self.$emit("refresh", null)
} else {
self.$message.warning(resp.msg)
}
this.$refs["form"].validate((valid) => {
if (!valid || this.formLoading) return
const url = this.formData.id ? "/platform/sys/unit/editDo" : "/platform/sys/unit/addDo"
if (this.currentNode) {
this.$set(this.formData, "parentId", this.currentData.id)
}
// 明确提交空字符串表示清空;避免 undefined 被请求序列化忽略。
const params = Object.assign({}, this.formData, {
aliasName: this.formData.aliasName || "",
birthdaySortNo: this.formData.birthdaySortNo == null ? "" : this.formData.birthdaySortNo
})
this.formLoading = true
this.$axios.post(url, params).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.doSearch()
this.$set(this, "dialogVisible", false)
this.$emit("refresh", null)
} else {
this.$message.warning(resp.msg)
}
}).finally(() => {
this.formLoading = false
})
})
})
},
async openEdit(id) {
console.log(this.currentData)
const resp = await this.$axios.post("/platform/sys/unit/edit/" + id)
if (resp.code === 0) {
this.formData = resp.data
this.dialogVisible = true
} else {
this.$message.warning(resp.msg)
}
openEdit(id) {
if (this.formLoading) return
this.formLoading = true
this.$axios.post("/platform/sys/unit/edit/" + id).then((resp) => {
if (resp.code === 0 && resp.data) {
this.$set(this, "formData", resp.data)
// 兼容历史空值,保留编号 0;数字控件使用 undefined 显示空输入。
this.$set(this.formData, "aliasName", resp.data.aliasName || "")
this.$set(this.formData, "birthdaySortNo", resp.data.birthdaySortNo == null ? undefined : resp.data.birthdaySortNo)
this.$set(this, "dialogVisible", true)
} else {
this.$message.warning(resp.msg)
}
}).finally(() => {
this.formLoading = false
})
},
async doDelete(id) {
const confirm = await this.$confirm("此操作将永久删除, 是否继续?", "提示", {
@@ -55,10 +55,36 @@ layout("/layouts/platform.html"){
:label="column.label" :width="column.width" :fixed="column.fixed"
:sortable="column.sortable"
header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" fixed="right" width="150" align="center">
<template slot-scope="{row}">
<el-button type="text" size="small" @click="openCampusForm(row)">修改所属校区</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<el-dialog title="修改所属校区" :visible.sync="campusDialogVisible" width="480px"
:close-on-click-modal="false" :close-on-press-escape="!formLoading" :show-close="!formLoading">
<el-form ref="campusFormRef" :model="campusForm" :rules="campusRules" label-width="100px">
<el-form-item label="工号">
<el-input :value="campusForm.loginname" readonly></el-input>
</el-form-item>
<el-form-item label="姓名">
<el-input :value="campusForm.username" readonly></el-input>
</el-form-item>
<el-form-item label="所属校区" prop="campusId">
<el-select v-model="campusForm.campusId" :disabled="formLoading" placeholder="请选择所属校区" style="width:100%">
<el-option v-for="item in campuses" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<div class="text-muted">此设置用于生日名单及签名表。</div>
</el-form>
<template slot="footer">
<el-button :disabled="formLoading" @click="closeCampusForm">取消</el-button>
<el-button type="primary" :loading="formLoading" @click="saveCampus">保存</el-button>
</template>
</el-dialog>
</guava>
</div>
@@ -86,6 +112,11 @@ layout("/layouts/platform.html"){
},
queryForm: {userStates:[],personTypes:[]},
birthdayRange: [],
campusDialogVisible: false,
campusForm: {userId:"",loginname:"",username:"",campusId:""},
campusRules: {
campusId: [{required:true,message:"请选择所属校区",trigger:"change"}]
},
campuses: [
{id:"hz",name:"杭州校区"},
{id:"cx",name:"长兴校区"}
@@ -109,6 +140,48 @@ layout("/layouts/platform.html"){
}
},
methods: {
// 回显列表中实际生效的生日校区,包括未设置标记时按单位推导出的校区。
openCampusForm(row) {
const campus = this.campuses.find(item => item.name === row.campusName || item.id === row.campusName)
this.$set(this, "campusForm", {
userId: row.id,
loginname: row.loginname,
username: row.username,
campusId: campus ? campus.id : ""
})
this.$set(this, "campusDialogVisible", true)
this.$nextTick(() => {
if (this.$refs.campusFormRef) this.$refs.campusFormRef.clearValidate()
})
},
// 保存中禁止关闭,避免请求未完成时切换到其他人员。
closeCampusForm() {
if (!this.formLoading) this.$set(this, "campusDialogVisible", false)
},
// 仅提交人员 ID 和校区编码,工号、姓名及校区名称由后端读取并校验。
saveCampus() {
if (this.formLoading) return
this.$refs.campusFormRef.validate((valid) => {
if (!valid) return
this.$set(this, "formLoading", true)
this.$axios.post("/platform/staffManage/birthday/export/saveCampus", {
userId: this.campusForm.userId,
campusId: this.campusForm.campusId
}).then((res) => {
if (res.code === 0) {
this.$message.success("所属校区修改成功")
this.$set(this, "campusDialogVisible", false)
this.doSearch()
} else {
this.$message.error(res.msg || "所属校区修改失败")
}
}).catch(() => {
this.$message.error("所属校区修改失败,请稍后重试")
}).finally(() => {
this.$set(this, "formLoading", false)
})
})
},
initOrganizationOptions() {
if (this.$auth.hasRoleOr("SYSADMIN, SCHOOL_UNION_ADMIN, SCHOOL_UNION_MEMBER_ADMIN")) {
this.$businessTool.listUnion().then((data) => {
@@ -110,7 +110,7 @@ const MEMBER_CHANGE = {
</el-select>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item v-else label="所属校区">
<el-descriptions-item v-else-if="!replaceWelfareWithCampus" label="所属校区">
<el-form-item prop="campus">
<dict-select style="width: 100%" placeholder="请选择所属校区" v-model="formData.campus"
:disabled="allowFields('campus')" code="USER_CAMPUS"></dict-select>
@@ -196,7 +196,14 @@ const MEMBER_CHANGE = {
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="福利会员状态">
<!-- 管理页在福利会员位置展示校区,保留三级单位;其他调用方沿用原字段布局。 -->
<el-descriptions-item v-if="replaceWelfareWithCampus" label="所属校区">
<el-form-item prop="campus">
<dict-select style="width: 100%" placeholder="请选择所属校区" v-model="formData.campus"
:disabled="allowFields('campus')" code="USER_CAMPUS"></dict-select>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item v-else label="福利会员状态">
<el-form-item prop="welfareMember">
<el-radio-group v-model="formData.welfareMember" size="small" class="welfare-member-radio-group"
style="display: flex; align-items: center; flex-wrap: nowrap; white-space: nowrap;">
@@ -288,6 +295,8 @@ const MEMBER_CHANGE = {
props: {
id: { type: String, default: '' },
showThreeUnit: { type: Boolean, default: false },
// 仅管理页启用福利会员与校区的展示替换,默认保持共用页面行为。
replaceWelfareWithCampus: { type: Boolean, default: false },
},
mixins: [initTableMixins],
store,
@@ -406,9 +415,7 @@ const MEMBER_CHANGE = {
.then((resp) => {
if (resp.code === 0) {
this.threeUnits = resp.data || []
if (this.threeUnits.length === 0) {
this.$message.warning("当前所属单位未配置三级单位")
}
// 未配置三级单位时保持空选项,允许继续编辑其他人员信息。
} else {
this.$message.error(resp.msg || "三级单位查询失败")
}
@@ -525,6 +532,7 @@ const MEMBER_CHANGE = {
this.$set(this.formData, "position", position)
this.$set(this.formData, "education", education)
this.$set(this.formData, "academicDegree", academicDegree)
// 与编辑接口及变更记录统一使用 campus,正确回显当前人员所属校区。
this.$set(this.formData, "campus", campus)
this.$set(this.formData, "threeUnitId", threeUnitId)
this.$set(this.formData, "userState", userState)
@@ -185,16 +185,9 @@ layout("/layouts/platform.html"){
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="福利会员" prop="member"
<!-- 所属校区展示列表接口返回的 campus 字段。 -->
<el-table-column align="center" header-align="center" label="所属校区" prop="campus"
show-overflow-tooltip sortable>
<template scope="{row}">
<span class="text-success " v-if="row.welfareMember==1">
<i class="fa fa-circle ml5"></i>
</span>
<span class="text-danger" v-else>
<i class="fa fa-circle ml5"></i>
</span>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="操作"
@@ -227,7 +220,7 @@ layout("/layouts/platform.html"){
</template>
<template #public>
<member-change ref="memberChangeRef" :show-three-unit="true" @do-back="$refs.guava.index()" @do-submit="doSubmit"></member-change>
<member-change ref="memberChangeRef" :show-three-unit="true" :replace-welfare-with-campus="true" @do-back="$refs.guava.index()" @do-submit="doSubmit"></member-change>
</template>
<template #edit>
@@ -319,6 +319,18 @@ layout("/layouts/platform.html"){
</el-form-item>
</template>
<el-form-item label="福利通知附件" prop="noticeAttachment">
<file-upload
:value.sync="formData.noticeAttachment"
:upload_number="1"
accept=".doc,.docx"
upload_mode="file"
upload_text="上传福利通知附件"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
</el-form-item>
<el-form-item label="封面图片" prop="cover">
<file-upload
:value.sync="formData.cover"
@@ -373,7 +385,6 @@ layout("/layouts/platform.html"){
formRules: {
name: [{ required: true, message: "必填", trigger: ["blur", "change"] }],
festival: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
provideTime: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
choiceTime: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
provideAddress: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
provideMode: [{ required: true, message: "必选", trigger: ["blur", "change"] }],
@@ -486,6 +497,7 @@ layout("/layouts/platform.html"){
noticePushMode: 1,
groupRequired: false,
groupSelectionConfigs: [],
noticeAttachment: "",
options: [],
welfareProjectSubjects: [],
flexibleGifts: [{}]
@@ -43,12 +43,10 @@ const welfareOption = {
<el-table-column align="center" header-align="center" label="系统默认选择" width="140">
<template slot-scope="{row}">
<el-radio
v-model="systemDefaultOption"
:label="row"
@change="setSystemDefault(row)">
&nbsp;
</el-radio>
<el-checkbox
:value="!!row.isSystemDefault"
@change="setSystemDefault(row, $event)">
</el-checkbox>
</template>
</el-table-column>
@@ -170,7 +168,6 @@ const welfareOption = {
data() {
return {
welfareList: [],
systemDefaultOption: null,
localGroupRequired: false,
descriptionDialog: {
visible: false,
@@ -187,10 +184,6 @@ const welfareOption = {
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);
}
},
watch: {
@@ -298,22 +291,16 @@ const welfareOption = {
return true;
},
// 每个福利项目只允许配置一个系统默认选项,保存时会随福利选项一并提交
setSystemDefault(defaultOption) {
this.welfareList.forEach((item) => {
this.$set(item, 'isSystemDefault', item === defaultOption);
});
// 每个福利选项独立维护系统默认状态,支持同时勾选多个选项或取消已有勾选
setSystemDefault(option, selected) {
this.$set(option, 'isSystemDefault', selected);
},
deleteOption(index) {
this.$confirm('确认删除该福利选项?', '提示', {
type: 'warning'
}).then(() => {
const deletedOption = this.welfareList[index];
this.welfareList.splice(index, 1);
if (this.systemDefaultOption === deletedOption) {
this.systemDefaultOption = null;
}
this.updateSortNumbers();
});
},
@@ -442,7 +429,7 @@ const welfareOption = {
transform: translate(-50%, -50%);
}
.welfare-option .el-radio__label {
.welfare-option .el-checkbox__label {
padding-left: 0;
}
@@ -135,11 +135,6 @@ layout("/layouts/platform.html"){
<span v-else>暂无</span>
</template>
<template scope="{row}" v-else-if="column.prop=='deliveryDate'">
<span v-if="row.deliveryDate">{{$moment(row.deliveryDate).format('YYYY-MM-DD')}}</span>
<span v-else>暂无</span>
</template>
<template scope="{row}" v-else-if="column.prop=='remark'">
<span v-if="row.remark">{{row.remark}}</span>
<span v-else>暂无</span>
@@ -224,7 +219,6 @@ layout("/layouts/platform.html"){
{ prop: "choiceTime", label: "选择时间" },
{ prop: "isChoose", label: "是否选择" },
{ prop: "gist_list", label: "所选福利" },
{ prop: "deliveryDate", label: "配送时间" },
{ prop: "remark", label: "备注" }
// { prop: "receiveAddress", label: "收货地址" }
],
@@ -289,15 +283,15 @@ layout("/layouts/platform.html"){
},
/**
* 配送日期到达后才显示评价入口;没有配送日期的历史数据不允许评价
* @param row 福利项目列表行,deliveryDate 为 yyyy-MM-dd 日期
* @return {boolean} 当前日期达到配送日期时返回 true
* 用户已选择福利且活动结束后显示评价入口
* @param row 福利项目列表行,choiceTimeEnd 为活动结束时间
* @return {boolean} 当前时间达到活动结束时间时返回 true
*/
canEvaluate(row) {
if (!row.isChoose || !row.deliveryDate) {
if (!row.isChoose || !row.choiceTimeEnd) {
return false
}
return this.$moment().startOf("day").valueOf() >= this.$moment(row.deliveryDate).startOf("day").valueOf()
return this.$moment().valueOf() >= this.$moment(row.choiceTimeEnd).valueOf()
},
/**
@@ -172,19 +172,6 @@ const optionSelect = {
</el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item prop="deliveryDate" label="配送时间">
<el-date-picker
v-model="contactForm.deliveryDate"
type="date"
value-format="yyyy-MM-dd"
format="yyyy-MM-dd"
placeholder="请选择配送时间"
:picker-options="deliveryDatePickerOptions"
style="width: 100%">
</el-date-picker>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item prop="remark" label="备注">
<el-input
@@ -285,7 +272,6 @@ const optionSelect = {
receiveAddress: "",
userName: "", // 收货人
userSign: "",
deliveryDate: "", // 配送日期,按 yyyy-MM-dd 提交
remark: "" // 用户自选和管理员代选共用的选择备注
},
contactRules: {
@@ -299,15 +285,8 @@ const optionSelect = {
message: "请输入正确的手机号码",
trigger: "blur"
}
],
deliveryDate: [
{required: true, message: "请选择配送时间", trigger: "change"}
]
},
deliveryDatePickerOptions: {
// 配送日期按天校验,选择当天允许提交。
disabledDate: (time) => time.getTime() < new Date().setHours(0, 0, 0, 0)
},
addressOptions: []
}
},
@@ -447,7 +426,6 @@ const optionSelect = {
receiveAddress: "",
userName: "",
userSign: "",
deliveryDate: "",
remark: ""
}
@@ -504,8 +482,6 @@ const optionSelect = {
// 本人选择按字段回退到登录人信息;代选只使用目标人员已有选择数据。
this.$set(this.contactForm, "mobile", firstSelection.mobile || (!this.isProxySelect ? currentUser.mobile || "" : ""))
this.$set(this.contactForm, "userName", firstSelection.userName || (!this.isProxySelect ? currentUser.username || "" : ""))
this.$set(this.contactForm, "deliveryDate", this.userSelection[0].deliveryDate
? this.$moment(this.userSelection[0].deliveryDate).format("YYYY-MM-DD") : "")
this.$set(this.contactForm, "remark", this.userSelection[0].remark || "")
} else {
if (this.isProxySelect) {
@@ -640,7 +616,6 @@ const optionSelect = {
userName: this.showReceivingContact ? this.contactForm.userName : "",
userSign: this.contactForm.userSign,
receiveAddress: this.showReceivingContact ? this.contactForm.receiveAddress : "",
deliveryDate: this.contactForm.deliveryDate,
remark: this.contactForm.remark
}
]
@@ -655,7 +630,6 @@ const optionSelect = {
userName: this.showReceivingContact ? this.contactForm.userName : "",
userSign: this.contactForm.userSign,
receiveAddress: this.showReceivingContact ? this.contactForm.receiveAddress : "",
deliveryDate: this.contactForm.deliveryDate,
remark: this.contactForm.remark
}))
}
@@ -29,8 +29,8 @@ layout("/layouts/platform.html"){
</el-select>
</search-item>
<search-item label="福利选项">
<el-select clearable filterable placeholder="请选择福利选项" style="width: 100%" v-model="pageForm.welfareOptionId">
<search-item label="所选套餐">
<el-select clearable filterable placeholder="请选择所选套餐" style="width: 100%" v-model="pageForm.welfareOptionId">
<el-option :key="item.id" :label="item.optionName" :value="item.id" v-for="item in welfareOptions"></el-option>
</el-select>
</search-item>
@@ -66,10 +66,33 @@ layout("/layouts/platform.html"){
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in unitOptions"></el-option>
</el-select>
</search-item>
<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
clearable
code="USER_PERSON_TYPE"
multiple
collapse-tags
placeholder="请选择人员类型"
v-model="pageForm.personTypes"
></dict-select>
</search-item>
<search-item label="在职状态">
<dict-select
clearable
code="USER_STATE"
multiple
collapse-tags
placeholder="请选择人员状态"
v-model="pageForm.userStates"
></dict-select>
</search-item>
<!-- <search-item label="人员分类">-->
<!-- <el-select clearable filterable placeholder="请选择人员分类" style="width: 100%" v-model="pageForm.aidFundMemberUserType">-->
<!-- <el-option :key="item.code" :label="item.name" :value="item.code" v-for="item in aidFundMemberUserTypeOptions"></el-option>-->
<!-- </el-select>-->
<!-- </search-item>-->
</search>
</el-card>
@@ -130,6 +153,7 @@ layout("/layouts/platform.html"){
></el-table-column>
<el-table-column label="收货地址" prop="receiveAddress" v-if="provideMode===3"></el-table-column>
<el-table-column label="备注" prop="remark" show-overflow-tooltip></el-table-column>
<el-table-column align="center" fixed="right" header-align="center" label="操作" width="100px">
<template slot-scope="{row}">
@@ -185,10 +209,13 @@ layout("/layouts/platform.html"){
pageForm: {
searchName: "username",
year: new Date().getFullYear().toString(),
isSelect: null
isSelect: null,
personTypes: [],
userStates: []
},
unionOptions: [],
unitOptions: [],
aidFundMemberUserTypeOptions: [],
projectOptions: [],
tableColumns: [
{ prop: "loginName", label: "工号" },
@@ -198,7 +225,6 @@ layout("/layouts/platform.html"){
{ prop: "welfareUnionName", label: "所属工会", sortable: true },
{ prop: "welfareUnitName", label: "所属单位", sortable: true },
{ prop: "selectedOptions", label: "所选福利", sortable: true },
{ prop: "deliveryDate", label: "配送时间", sortable: true },
{ prop: "mobile", label: "联系电话", sortable: true }
],
optionSelectVisible: false,
@@ -213,6 +239,13 @@ layout("/layouts/platform.html"){
}
},
computed: {
noticeDetailUrl() {
if (!this.pageForm.projectId) {
return ""
}
const appDomain = typeof APP_DOMAIN !== "undefined" && APP_DOMAIN ? APP_DOMAIN : window.location.origin
return appDomain.replace(/\/+$/, "") + "/platform/welfare/notice/index?id=" + encodeURIComponent(this.pageForm.projectId)
},
welfareOptions() {
if (this.pageForm.projectId) {
return this.projectOptions.find((item) => item.id === this.pageForm.projectId)?.options
@@ -286,7 +319,7 @@ layout("/layouts/platform.html"){
this.$message.warning("请先选择福利项目")
return
}
this.$set(this.messageDialog, "messageContent", "")
this.$set(this.messageDialog, "messageContent", "查看详情:" + this.noticeDetailUrl)
this.$set(this.messageDialog, "visible", true)
this.$nextTick(() => {
if (this.$refs.messageForm) {
@@ -312,7 +345,7 @@ layout("/layouts/platform.html"){
this.$message.warning("请输入有效的消息内容")
return
}
this.messageSending = true
this.$set(this, "messageSending", true)
this.$axios.post("/platform/welfare/selection/situation/sendMessage", {
pageForm: JSON.stringify(this.pageForm),
messageContent: messageContent
@@ -324,67 +357,7 @@ layout("/layouts/platform.html"){
this.$message.error(res.msg)
}
}).finally(() => {
this.messageSending = false
})
})
},
// 按当前页面全部查询条件导出签收单模版,后台会再次叠加登录人的数据权限。
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) {
this.$message.warning("请先选择福利项目")
return
}
this.$set(this.messageDialog, "messageContent", "")
this.$set(this.messageDialog, "visible", true)
this.$nextTick(() => {
if (this.$refs.messageForm) {
this.$refs.messageForm.clearValidate()
}
})
},
closeMessageDialog(done) {
this.$set(this.messageDialog, "visible", false)
if (typeof done === "function") {
done()
}
},
sendMessage() {
this.$refs.messageForm.validate((valid) => {
if (!valid) {
return
}
const messageContent = (this.messageDialog.messageContent || "").trim()
if (!messageContent) {
this.$message.warning("请输入有效的消息内容")
return
}
this.messageSending = true
this.$axios.post("/platform/welfare/selection/situation/sendMessage", {
pageForm: JSON.stringify(this.pageForm),
messageContent: messageContent
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.closeMessageDialog()
} else {
this.$message.error(res.msg)
}
}).finally(() => {
this.messageSending = false
this.$set(this, "messageSending", false)
})
})
},
@@ -401,6 +374,9 @@ layout("/layouts/platform.html"){
this.getWelfareList()
this.unionOptions = await this.$businessTool.listUnion()
this.unitOptions = await this.$businessTool.listUnit()
// 人员分类下拉仅在当前页面排除离退休人员,不影响全局字典及其他页面。
this.$set(this, "aidFundMemberUserTypeOptions", (await this.$businessTool.getDictOptions("AIDFUND_MEMBER_USER_TYPE"))
.filter((item) => item.code !== "离退休人员"))
}
})
</script>
@@ -33,15 +33,21 @@ layout("/layouts/platform.html"){
<el-card class="mt10" shadow="never" v-loading="tableLoading">
<table-tool label="工会列表">
<el-button @click="exportByWelfareOptions" icon="el-icon-printer" size="small" type="primary">按福利选项导出</el-button>
<!-- V4 原导出按钮保留,不再作为当前页面入口。 -->
<!-- <el-button @click="exportByWelfareOptions" icon="el-icon-printer" size="small" type="primary">按福利选项导出</el-button> -->
<el-button :disabled="!pageForm.projectId" @click="remindUnSelectedPlaceholder" icon="el-icon-bell" size="small" type="primary">
提醒未选择人员
</el-button>
<template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN','SCHOOL_UNION_WELFARE_ADMIN'])">
<el-button :disabled="!pageForm.projectId" @click="exportSummary" icon="el-icon-printer" size="small" type="primary">
导出汇总表
<!-- V4 原汇总导出按钮保留,不再作为当前页面入口。 -->
<!-- <el-button :disabled="!pageForm.projectId" @click="exportSummary" icon="el-icon-printer" size="small" type="primary">导出汇总表</el-button> -->
<el-button :disabled="!pageForm.projectId" @click="allExportReceiveDetailByUnionIdWord" icon="el-icon-printer" size="small" type="primary">
导出各分工会福利签领表
</el-button>
</template>
<el-button :disabled="!pageForm.projectId" @click="exportByWelfareOptionsV3" icon="el-icon-printer" size="small" type="primary">按品牌导出</el-button>
<el-button :disabled="!pageForm.projectId" @click="exportReceiveDetailByUnionId(true)" icon="el-icon-printer" size="small" type="primary">导出已选名单</el-button>
<el-button :disabled="!pageForm.projectId" @click="exportReceiveDetailByUnionId(false)" icon="el-icon-printer" size="small" type="primary">导出未选名单</el-button>
</table-tool>
<el-table
:data="tableData"
@@ -74,6 +80,12 @@ layout("/layouts/platform.html"){
<el-link @click="openUnSelected(row)" type="primary">{{row.unSelectedNum}}</el-link>
</template>
</el-table-column>
<el-table-column align="center" fixed="right" header-align="center" label="操作" width="260">
<template slot-scope="{row}">
<el-button plain size="mini" type="primary" @click="exportReceiveDetailByUnionIdWord(row)">导出福利签领表</el-button>
<el-button size="mini" type="primary" @click="openSelected(row)">已选人员</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</template>
@@ -113,7 +125,7 @@ layout("/layouts/platform.html"){
remindUnSelectedPlaceholder() {
this.$message.info("提醒未选择人员功能待开发")
},
// 导出汇总表
// V4 原汇总表导出方法保留,页面已改用 V3 风格的导出入口。
exportSummary() {
this.$downLoad("/platform/welfare/statistics/exportSummary", {
projectId: this.pageForm.projectId
@@ -129,7 +141,7 @@ layout("/layouts/platform.html"){
this.$refs.unSelectedUserRef.onOpen(this.pageForm.projectId, row)
},
// 按福利选项导出
// V4 原按福利选项导出方法保留,页面已改用 V3 的按品牌导出入口。
exportByWelfareOptions() {
this.$downLoad("/platform/welfare/statistics/exportByWelfareOptions", {
projectId: this.pageForm.projectId,
@@ -137,6 +149,38 @@ layout("/layouts/platform.html"){
})
},
// V3 按品牌导出:按套餐拆分工作表,并传递当前分工会筛选范围。
exportByWelfareOptionsV3() {
this.$downLoad("/platform/welfare/statistics/exportByWelfareOptions", {
projectId: this.pageForm.projectId,
unionId: this.pageForm.unionId
})
},
// V3 已选、未选名单导出:未选择分工会时由后台导出当前数据权限内的全部人员。
exportReceiveDetailByUnionId(flag) {
this.$downLoad("/platform/welfare/statistics/exportReceiveDetailByUnionId", {
projectId: this.pageForm.projectId,
unionId: this.pageForm.unionId,
flag: flag
})
},
// V3 单个分工会福利签领表导出。
exportReceiveDetailByUnionIdWord(row) {
this.$downLoad("/platform/welfare/statistics/exportReceiveDetailByUnionIdWord", {
projectId: this.pageForm.projectId,
unionId: row.id
})
},
// V3 全部分工会福利签领表导出,后台生成 ZIP 文件。
allExportReceiveDetailByUnionIdWord() {
this.$downLoad("/platform/welfare/statistics/allExportReceiveDetailByUnionIdWord", {
projectId: this.pageForm.projectId
})
},
async pageData() {
this.tableLoading = true
const resp = await this.$axios.post("/platform/welfare/statistics/pageData", this.pageForm)
@@ -6,7 +6,7 @@ const selectedUser = {
<el-input
v-model="pageForm.searchKeyword"
placeholder="请输入姓名或工号"
style="width: 200px;"
style="width: 300px;"
size="small"
clearable
@keyup.enter.native="doSearch"
@@ -51,7 +51,7 @@ const selectedUser = {
{ label: "生日", prop: "birthday" },
{ prop: "userState", label: "在职状态", width: 120, sortable: true },
{ prop: "personType", label: "人员类型", width: 120, sortable: true },
{ prop: "postDoctoralJoinDate", label: "进站时间", sortable: true },
// { prop: "postDoctoralJoinDate", label: "进站时间", sortable: true },
{ label: "单位", prop: "welfareUnitName" },
{ label: "手机号", prop: "mobile" },
{ label: "所选福利", prop: "selectOptionName" }
@@ -63,7 +63,7 @@ const addUser = {
</search-item>
<search-item label="所属单位">
<el-select @change="unitChange" clearable
<el-select clearable
filterable
placeholder="请选择所属单位" style="width: 100%"
v-model="pageForm.unitId">
@@ -76,18 +76,6 @@ const addUser = {
</el-select>
</search-item>
<search-item label="三级单位">
<el-select clearable filterable placeholder="请先选择所属单位" style="width: 100%"
v-model="pageForm.threeUnitId">
<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 clearable code="USER_PERSON_TYPE"
multiple
@@ -163,7 +151,6 @@ const addUser = {
birthMonths: [],
unionId: null,
unitId: null,
threeUnitId: null,
personTypes: [],
preparedBys: [],
userStates: [],
@@ -172,7 +159,6 @@ const addUser = {
sexOptions: ["男", "女"],
unionOptions: [],
unitOptions: [],
threeUnitOptions: [],
tableColumns: [
{ prop: "loginname", label: "工号" },
{ prop: "username", label: "姓名" },
@@ -181,8 +167,7 @@ const addUser = {
{ prop: "personType", label: "人员类型", sortable: true },
{ prop: "userState", label: "在职状态", sortable: true },
{ prop: "unionName", label: "所属工会", sortable: true },
{ prop: "unitName", label: "所属单位", sortable: true },
{ prop: "threeUnitName", label: "三级单位", sortable: true }
{ prop: "unitName", label: "所属单位", sortable: true }
]
}
},
@@ -204,8 +189,6 @@ const addUser = {
this.$set(this.pageForm, "birthMonths", [])
this.$set(this.pageForm, "unionId", null)
this.$set(this.pageForm, "unitId", null)
this.$set(this.pageForm, "threeUnitId", null)
this.$set(this, "threeUnitOptions", [])
this.$set(this.pageForm, "personTypes", [])
this.$set(this.pageForm, "preparedBys", [])
this.$set(this.pageForm, "userStates", [])
@@ -214,7 +197,6 @@ const addUser = {
async unionIdChange(val) {
this.$set(this.pageForm, "unitId", null)
this.unitChange(null)
if (val) {
this.$set(this, "unitOptions", await this.$businessTool.listUnit(val))
} else {
@@ -222,26 +204,6 @@ const addUser = {
}
},
// 所属单位改变后,仅加载该单位直属的三级单位,避免跨单位筛选。
unitChange(unitId) {
this.$set(this.pageForm, "threeUnitId", null)
this.$set(this, "threeUnitOptions", [])
if (!unitId) {
return
}
this.$axios.post("/platform/sys/unit/child", { pid: unitId })
.then((res) => {
if (res.code === 0) {
this.$set(this, "threeUnitOptions", res.data || [])
} else {
this.$message.error(res.msg || "三级单位查询失败")
}
})
.catch(() => {
this.$message.error("三级单位查询失败,请稍后重试")
})
},
pageData() {
this.tableLoading = true
this.$axios
@@ -25,7 +25,7 @@ layout("/layouts/platform.html"){
</search-item>
<search-item label="福利项目">
<el-select :clearable="false" filterable placeholder="请选择福利项目" style="width: 100%" v-model="pageForm.projectId">
<el-select :clearable="false" filterable placeholder="请选择福利项目" style="width: 100%" v-model="pageForm.projectId" @change="projectChange">
<el-option :key="item.id" :label="item.name" :value="item.id" v-for="item in projectSelectOptions"></el-option>
</el-select>
</search-item>
@@ -133,11 +133,6 @@ layout("/layouts/platform.html"){
<!-- <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>-->
@@ -148,6 +143,35 @@ layout("/layouts/platform.html"){
<el-card class="mt10 welfare-list-card" shadow="never">
<table-tool :app="this" label="福利名单">
<el-select
:disabled="!isExpressWelfareProject"
clearable
filterable
placeholder="请选择套餐"
size="small"
style="width: 180px"
v-model="pageForm.optionId"
>
<el-option :key="item.id" :label="item.optionName" :value="item.id" v-for="item in pageFormProject"></el-option>
</el-select>
<el-button
:disabled="!isExpressWelfareProject || !pageForm.optionId"
@click="doExcelByOptionId"
icon="el-icon-printer"
size="small"
type="primary"
>
导出报销表
</el-button>
<el-button
:disabled="!isExpressWelfareProject"
@click="openImportExpressExcel"
icon="el-icon-upload2"
size="small"
type="primary"
>
导入快递单号
</el-button>
<el-button :disabled="!pageForm.projectId" :loading="batchDeleteLoading" @click="deleteSearchUsers" icon="el-icon-delete" size="small" type="danger">
删除人员
</el-button>
@@ -254,6 +278,8 @@ layout("/layouts/platform.html"){
width="700px"
:extra_params="{projectId:pageForm.projectId}"
></excel-import>
<import-courier-number-dialog ref="importCourierNumberRef"></import-courier-number-dialog>
</div>
<script nonce="${cspNonce!}">
@@ -270,11 +296,15 @@ layout("/layouts/platform.html"){
if (this.pageForm.projectId) {
const projectInfo = this.projectSelectOptions.find((v) => v.id === this.pageForm.projectId)
if (projectInfo) {
return projectInfo.optionList
return projectInfo.options || []
}
return null
return []
}
return null
return []
},
isExpressWelfareProject() {
const projectInfo = this.projectSelectOptions.find((v) => v.id === this.pageForm.projectId)
return projectInfo && projectInfo.provideMode === 3
},
canSyncUnion() {
const projectInfo = this.projectSelectOptions.find((v) => v.id === this.pageForm.projectId)
@@ -288,7 +318,8 @@ layout("/layouts/platform.html"){
add_welfare_list_by_select: ADD_WELFARE_LIST_BY_SELECT,
"file-import": httpVueLoader("/components/plugins/sysImport/index.vue?v=" + new Date().getTime()),
"add-user": addUser,
"update-remark": updateRemark
"update-remark": updateRemark,
"import-courier-number-dialog": IMPORT_COURIER_NUMBER_DIALOG
},
data() {
return {
@@ -326,6 +357,25 @@ layout("/layouts/platform.html"){
}
},
methods: {
// 切换福利项目后清空旧套餐,避免将上一个项目的套餐用于导出或导入。
projectChange() {
this.$set(this.pageForm, "optionId", null)
this.doSearch()
},
// 导出当前福利项目中指定套餐的报销表。
doExcelByOptionId() {
this.$downLoad("/platform/welfare/list/mange/doExcelByOptionId", {
projectId: this.pageForm.projectId,
optionId: this.pageForm.optionId
})
},
// 打开快递单号导入弹窗,并把当前福利项目的套餐列表传给弹窗选择。
openImportExpressExcel() {
this.$refs.importCourierNumberRef.openImportExpressExcel(this.pageForm.projectId, this.pageFormProject)
},
// 导出名单
openExport() {
this.$downLoad("/platform/welfare/list/mange/exportXlsx", {
@@ -430,6 +480,7 @@ layout("/layouts/platform.html"){
getWelfareList() {
this.$axios.post("/platform/welfare/project/mange/getWelfareList", { year: this.pageForm.year }).then((res) => {
this.projectSelectOptions = res.data
this.$set(this.pageForm, "optionId", null)
if (res.data && res.data.length > 0) {
this.$set(this.pageForm, "projectId", res.data[0].id)
} else {
@@ -22,7 +22,7 @@ layout("/layouts/platform_h5.html"){
@keyframes confetti-fall { 0% { transform: translate3d(0,-20px,0) rotate(0); opacity: 0; }
12% { opacity: 1; } 100% { transform: translate3d(35px,62vh,0) rotate(540deg); opacity: 0; } }
.greeting-zone { position: relative; z-index: 4; height: 48%; display: flex; flex-direction: column; align-items: center;
justify-content: flex-start; padding: 0 24px calc(20px + env(safe-area-inset-bottom)); box-sizing: border-box; }
justify-content: flex-start; padding: 24px 24px calc(20px + env(safe-area-inset-bottom)); box-sizing: border-box; /* 下移祝福卡片,为蛋糕底部留出间距。 */ }
.greeting-card { width: 100%; box-sizing: border-box; padding: 20px 20px 16px; border: 1px solid rgba(164,60,26,.3);
border-radius: 18px; background: rgba(255,249,228,.92); box-shadow: 0 12px 30px rgba(109,29,10,.22);
text-align: center; animation: card-enter .7s ease-out both; }
@@ -115,7 +115,7 @@ layout("/layouts/platform_h5.html"){
生日快乐,幸福安康!
</div>
</div>
<button class="receive-button" @click="openCoupon">领取生日福利</button>
<button v-if="hasCouponId" class="receive-button" @click="openCoupon">领取生日福利</button>
</div>
<button type="button" class="music-button" :class="{playing:musicPlaying}"
@@ -165,6 +165,8 @@ layout("/layouts/platform_h5.html"){
return {
appName: "${AppName!}",
userName: "${userName!}",
// 仅地址参数 id 非空时提供福利入口,后台配置的贺卡不作为按钮显示依据。
hasCouponId: "${fileId!}".trim().length > 0,
pageLoading: false,
showMusicPrompt: false,
musicPromptResolved: false,
@@ -230,7 +232,7 @@ layout("/layouts/platform_h5.html"){
}
},
openCoupon() {
if (this.showCoupon) return
if (!this.hasCouponId || this.showCoupon) return
this.$set(this, "showCoupon", true)
window.h5PopupHistory.open(this.popupHistoryToken, "birthdayCoupon")
},
@@ -1351,15 +1351,15 @@ layout("/layouts/platform_h5.html"){
},
/**
* 配送日期到达后才显示移动端评价入口;历史记录未设置配送日期时保持隐藏
* @param row 我的福利列表行,deliveryDate 为 yyyy-MM-dd 日期
* @return {boolean} 当前日期达到配送日期时返回 true
* 当前用户已选择福利且项目活动结束后显示移动端评价入口
* @param row 我的福利列表行,choiceTimeEnd 为活动结束时间
* @return {boolean} 当前时间达到活动结束时间时返回 true
*/
canEvaluate(row) {
if (!row || !row.deliveryDate) {
if (!row || !row.isChoose || !row.choiceTimeEnd) {
return false
}
return this.$moment().startOf("day").valueOf() >= this.$moment(row.deliveryDate).startOf("day").valueOf()
return this.$moment().valueOf() >= this.$moment(row.choiceTimeEnd).valueOf()
},
/**
@@ -0,0 +1,97 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
.welfare-notice-page {
min-height: 100vh;
background: #f7f8fa;
}
.welfare-notice-card {
margin: 12px;
padding: 14px;
background: #ffffff;
border-radius: 8px;
}
.welfare-notice-title {
margin-bottom: 12px;
color: #323233;
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
.welfare-notice-viewer {
width: 100%;
height: calc(100vh - 150px);
border: 0;
background: #ffffff;
}
.welfare-notice-empty {
padding: 50px 0;
}
</style>
<div id="app" v-cloak class="welfare-notice-page" v-loading="pageLoading">
<van-nav-bar title="福利通知" left-text="返回" left-arrow fixed placeholder @click-left="back"></van-nav-bar>
<div class="welfare-notice-card" v-if="notice.fileId">
<div class="welfare-notice-title">{{notice.projectName}}</div>
<iframe class="welfare-notice-viewer" :src="viewerUrl" :title="notice.fileName"></iframe>
<van-button block type="primary" plain @click="download">下载原文件</van-button>
</div>
<van-empty v-else-if="!pageLoading" class="welfare-notice-empty" description="暂无福利通知附件"></van-empty>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
const query = new URLSearchParams(window.location.search)
return {
projectId: query.get("id") || "",
notice: {},
pageLoading: false
}
},
computed: {
viewerUrl() {
if (!this.notice.previewUrl) {
return ""
}
return "/assets/platform/plugins/pdfJs/web/viewer.html?file=" + encodeURIComponent(this.notice.previewUrl)
}
},
methods: {
back() {
window.history.back()
},
loadNotice() {
this.$set(this, "pageLoading", true)
this.$axios.post("/platform/welfare/notice/detailData", {id: this.projectId}).then((res) => {
if (res.code === 0) {
this.$set(this, "notice", res.data || {})
} else {
this.$toast.fail(res.msg)
}
}).finally(() => {
this.$set(this, "pageLoading", false)
})
},
download() {
if (this.notice.attachmentUrl) {
window.location.href = this.notice.attachmentUrl
}
}
},
created() {
this.loadNotice()
}
})
</script>
<!--#
}
#-->
@@ -242,7 +242,8 @@ layout("/layouts/platform_h5.html"){
.welfare-detail-link {
display: inline-flex;
height: 20px;
margin-left: 6px;
margin-left: auto;
padding-left: 6px;
flex: none;
align-items: center;
color: #1989fa;
@@ -827,8 +828,8 @@ layout("/layouts/platform_h5.html"){
<span>{{ optionRankText(option) }}</span>
</div>
<div class="welfare-option-desc-row" v-if="optionBrief(option)">
<div class="welfare-option-desc">
<div class="welfare-option-desc-row">
<div class="welfare-option-desc" v-if="optionBrief(option)">
{{ optionBrief(option) }}
</div>
<div class="welfare-detail-link" @click.stop="showOptionDetail(option)">
@@ -847,7 +848,8 @@ layout("/layouts/platform_h5.html"){
integer
disable-input
: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)"
input-width="40px"
button-size="22px"
@@ -918,16 +920,6 @@ layout("/layouts/platform_h5.html"){
maxlength="11"
required
></van-field>
<van-field
v-model="formData.deliveryDate"
label="配送时间"
placeholder="请选择配送时间"
readonly
clickable
is-link
@click="openDeliveryCalendar"
required
></van-field>
<van-field
v-model="formData.remark"
label="备注"
@@ -990,27 +982,15 @@ layout("/layouts/platform_h5.html"){
</div>
</van-popup>
<!-- 配送日期使用 Vant 日历快捷选择,点击日期后立即确认;弹层同步接入全局历史栈。 -->
<van-calendar
v-model="showDeliveryCalendar"
title="选择配送时间"
type="single"
color="#1989fa"
:show-confirm="false"
:close-on-click-overlay="false"
:min-date="deliveryCalendarMinDate"
:default-date="deliveryCalendarDefaultDate"
@confirm="confirmDeliveryDate"
@close="handleHistoryLayerComponentClose('delivery-calendar')">
</van-calendar>
<!-- 选项详情弹窗 -->
<van-action-sheet v-model="showOptionDetailDialog" class="welfare-detail-sheet"
:style="{ height: '78%' }" :close-on-click-overlay="true"
:title="selectedOption ? selectedOption.optionName : ''" cancel-text="取消"
@close="handleHistoryLayerComponentClose('detail')">
@close="handleHistoryLayerComponentClose('detail')">
<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>
</van-action-sheet>
@@ -1051,8 +1031,6 @@ layout("/layouts/platform_h5.html"){
projectId: "",
selectedRadioId: "", // 单选模式下选中的选项ID
showConfirmDialog: false,
showDeliveryCalendar: false, // 配送日期日历弹层
deliveryCalendarDefaultDate: new Date(), // 日历打开时默认定位的日期
isSubmitting: false,
hasSubmittedBefore: false, // 是否之前提交过
showOptionDetailDialog: false, // 选项详情弹窗
@@ -1065,7 +1043,6 @@ layout("/layouts/platform_h5.html"){
address: "",// 收货地址
receiveAddress: "", // 确认后的完整收货地址
userName: "",//收货人
deliveryDate: "", // 配送日期,按 yyyy-MM-dd 提交
remark: "" // 用户选择备注
},
@@ -1077,11 +1054,6 @@ layout("/layouts/platform_h5.html"){
computed: {
// Vant 日历最小可选日期为当天零点,允许用户选择当天配送。
deliveryCalendarMinDate() {
return this.$moment().startOf("day").toDate()
},
// 系统默认福利必须被保留,管理端限制每个项目最多配置一个。
systemDefaultOption() {
if (!this.projectInfo.options) return null
@@ -1142,7 +1114,7 @@ layout("/layouts/platform_h5.html"){
return Math.max(0, this.selectionLimit - this.selectedCount)
},
// 意向选择不采集收货人、联系电话和收货地址,配送时间及备注仍按原业务要求保留。
// 意向选择不采集收货人、联系电话和收货地址,备注仍按原业务要求保留。
showReceivingContact() {
return Number(this.projectInfo.provideMode) !== 2
},
@@ -1174,7 +1146,6 @@ layout("/layouts/platform_h5.html"){
// 根据浏览器历史中的弹层栈统一显示或关闭弹框,保证返回键与页面状态一致。
syncHistoryLayerStack(stack) {
this.showConfirmDialog = stack.includes("confirm")
this.$set(this, "showDeliveryCalendar", stack.includes("delivery-calendar"))
this.showAddressPopup = stack.includes("address")
this.showOptionDetailDialog = stack.includes("detail")
},
@@ -1212,36 +1183,10 @@ layout("/layouts/platform_h5.html"){
setHistoryLayerVisible(layerName, visible) {
if (layerName === "confirm") this.showConfirmDialog = visible
if (layerName === "delivery-calendar") this.$set(this, "showDeliveryCalendar", visible)
if (layerName === "address") this.showAddressPopup = visible
if (layerName === "detail") this.showOptionDetailDialog = visible
},
/**
* 打开配送日期日历。已有 deliveryDate 时定位到该日期,否则默认定位当天;无返回值。
* 日历作为确认选择弹层的下一层历史记录,系统返回键只关闭日历。
*/
openDeliveryCalendar() {
const selectedDate = this.formData.deliveryDate
? this.$moment(this.formData.deliveryDate, "YYYY-MM-DD").toDate()
: new Date()
// 历史配送日期早于今天时定位到今天,避免默认日期超出日历可选范围。
const defaultDate = this.$moment(selectedDate).startOf("day").valueOf()
< this.$moment().startOf("day").valueOf() ? new Date() : selectedDate
this.$set(this, "deliveryCalendarDefaultDate", defaultDate)
this.openHistoryLayer("delivery-calendar")
},
/**
* 快捷选中配送日期。date 为 van-calendar 返回的 Date,写入 yyyy-MM-dd 字符串后同步回退日历历史;无返回值。
*
* @param date Vant 日历当前选中的日期
*/
confirmDeliveryDate(date) {
this.$set(this.formData, "deliveryDate", this.$moment(date).format("YYYY-MM-DD"))
this.closeHistoryLayer("delivery-calendar")
},
optionRankText(option) {
if (option.rankNo) {
return "本次福利排行榜第" + option.rankNo + "名"
@@ -1255,6 +1200,20 @@ layout("/layouts/platform_h5.html"){
const text = (div.textContent || div.innerText || "").replace(/\s+/g, " ").trim()
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) {
const option = this.projectInfo.options[index];
if (option.isSystemDefault && Number(newValue) < 1) {
@@ -1262,6 +1221,12 @@ layout("/layouts/platform_h5.html"){
this.$toast.fail("系统默认福利不可取消")
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;
// 计算其他所有选项的总和(不包括当前修改的选项)
@@ -1282,7 +1247,7 @@ layout("/layouts/platform_h5.html"){
// 如果当前选项的值大于允许的最大值,则调整它
if (option.selectNum > maxAllowedForThisOption) {
option.selectNum = maxAllowedForThisOption;
this.$set(option, "selectNum", maxAllowedForThisOption)
}
// 如果当前选项的值小于或等于允许的最大值(例如,用户减少了数量导致总数超标,但实际上减少后并未超过 maxSelect,
// 这种情况理论上不应该在 change 时发生,因为 change 是值改变后触发的,且 change 后的值导致了超标),
@@ -1306,6 +1271,44 @@ layout("/layouts/platform_h5.html"){
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 福利选项索引
@@ -1320,16 +1323,28 @@ layout("/layouts/platform_h5.html"){
return
}
if (option.isSystemDefault) return
this.selectedRadioId = ""
this.projectInfo.options.forEach((item) => {
item.selectNum = 0
})
this.syncRadioOptionSelection("")
return
}
this.$set(option, "selectNum", 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) {
if (this.isDeadlinePassed) {
@@ -1387,10 +1402,8 @@ layout("/layouts/platform_h5.html"){
this.userSelection = resp.data
this.hasSubmittedBefore = this.userSelection.length > 0
// 配送时间和备注属于整次选择,统一从首条选择记录回显。
// 备注属于整次选择,统一从首条选择记录回显。
if (this.userSelection && this.userSelection.length > 0) {
this.$set(this.formData, "deliveryDate", this.userSelection[0].deliveryDate
? this.$moment(this.userSelection[0].deliveryDate).format("YYYY-MM-DD") : "")
this.$set(this.formData, "remark", this.userSelection[0].remark || "")
}
@@ -1427,11 +1440,7 @@ layout("/layouts/platform_h5.html"){
if (this.projectInfo.options && this.userSelection.length > 0) {
// 单选模式
if (this.projectInfo.isCheckBox === "radio" && this.userSelection[0]) {
this.selectedRadioId = this.userSelection[0].selectOptionId
const selectedOption = this.projectInfo.options.find((opt) => opt.id === this.selectedRadioId)
if (selectedOption) {
selectedOption.selectNum = 1
}
this.syncRadioOptionSelection(this.userSelection[0].selectOptionId)
}
// 多选模式
else {
@@ -1543,11 +1552,6 @@ layout("/layouts/platform_h5.html"){
return
}
// 配送时间是评价开放依据,提交前必须确保不早于当前选择日期。
if (!this.validateDeliveryDate()) {
return
}
if (this.isSubmitting) return
this.isSubmitting = true
@@ -1568,7 +1572,6 @@ layout("/layouts/platform_h5.html"){
selectNum: 1,
mobile: this.showReceivingContact ? this.formData.mobile : "",
userName: this.showReceivingContact ? this.formData.userName : "",
deliveryDate: this.formData.deliveryDate,
remark: this.formData.remark
}
]
@@ -1581,7 +1584,6 @@ layout("/layouts/platform_h5.html"){
selectNum: option.selectNum,
mobile: this.showReceivingContact ? this.formData.mobile : "",
userName: this.showReceivingContact ? this.formData.userName : "",
deliveryDate: this.formData.deliveryDate,
remark: this.formData.remark
}))
}
@@ -1653,26 +1655,6 @@ layout("/layouts/platform_h5.html"){
return true;
},
/**
* 校验配送日期。deliveryDate 应传 yyyy-MM-dd,返回布尔值表示是否允许继续提交。
* 日期按天比较,配送日期等于选择当天时允许提交。
*
* @return {boolean} 配送日期有效返回 true,否则提示错误并返回 false
*/
validateDeliveryDate() {
if (!this.formData.deliveryDate) {
this.$toast.fail("请选择配送时间")
return false
}
const deliveryDate = this.$moment(this.formData.deliveryDate).startOf("day").valueOf()
const currentDate = this.$moment().startOf("day").valueOf()
if (deliveryDate < currentDate) {
this.$toast.fail("配送时间不能早于选择时间")
return false
}
return true
},
// 提交选择
submitSelection() {
if (!this.hasSelection) {
@@ -1708,22 +1690,11 @@ layout("/layouts/platform_h5.html"){
return
}
if (this.systemDefaultOption && this.systemDefaultOption.id !== optionId) {
this.selectedRadioId = this.systemDefaultOption.id
this.syncRadioOptionSelection(this.systemDefaultOption.id)
this.$toast.fail("系统默认福利不可取消")
return
}
this.selectedRadioId = optionId
// 重置所有选项的selectNum
if (this.projectInfo.options) {
this.projectInfo.options.forEach((option) => {
if (option.id === optionId) {
option.selectNum = 1
} else {
option.selectNum = 0
}
})
}
this.syncRadioOptionSelection(optionId)
},
// 默认项只在首次选择时自动带入,已有历史选择不自动覆盖。
@@ -505,10 +505,6 @@ layout("/layouts/platform_h5.html"){
<van-icon name="clock-o"></van-icon>
<span>选择时间:未设置</span>
</div>
<div class="welfare-list-card__time" v-if="Number(row.isChoose) === 1">
<van-icon name="logistics"></van-icon>
<span>配送时间:{{ row.deliveryDate ? $moment(row.deliveryDate).format('YYYY-MM-DD') : '暂无' }}</span>
</div>
<div class="welfare-list-card__time" v-if="Number(row.isChoose) === 1 && row.remark">
<van-icon name="notes-o"></van-icon>
<span>备注:{{ row.remark }}</span>