16 Commits
65 changed files with 3758 additions and 668 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;
@@ -157,12 +157,14 @@ public class RecuperationEvaluateStatisticsController {
List<ExcelExportEntity> columns = new ArrayList<>();
columns.add(new ExcelExportEntity("序号", "no", 10));
columns.add(new ExcelExportEntity("报名方式", "typeName", 12));
columns.add(new ExcelExportEntity("方案名称", "schemeName", 24));
columns.add(new ExcelExportEntity("方案名称", "projectName", 24));
columns.add(new ExcelExportEntity("旅行社", "travelAgencyName", 22));
columns.add(new ExcelExportEntity("评价人", "userName", 14));
columns.add(new ExcelExportEntity("所属单位", "unitName", 24));
columns.add(new ExcelExportEntity("旅行社评分", "evaluationForTravelAgency", 14));
columns.add(new ExcelExportEntity("住宿/酒店评分", "evaluationForAccommodation", 16));
String accommodationLabel = "线路".equals(typeName) ? "住宿评分"
: ("灵活组团".equals(typeName) || "定点".equals(typeName) ? "酒店评分" : "住宿/酒店评分");
columns.add(new ExcelExportEntity(accommodationLabel, "evaluationForAccommodation", 16));
columns.add(new ExcelExportEntity("行程评分", "evaluationForJourney", 14));
columns.add(new ExcelExportEntity("餐饮评分", "evaluationForDining", 14));
columns.add(new ExcelExportEntity("交通评分", "evaluationForTransportation", 14));
@@ -62,10 +62,11 @@ public class RecuperationFlexibleGroupQueryController {
return Result.success(flexibleGroupService.querySignUsers(pageForm, groupId, groupLeaderUserId, groupLeaderLoginName, noGroupLeader));
}
/** pageForm 传分页信息,groupId 传灵活组团 ID;data 返回全部报名人员的分页结构。 */
@At
@SaCheckLogin
public Result getAllSignUser(PageForm pageForm, String groupId) {
return Result.success(flexibleGroupService.querySignUsers(pageForm, groupId, null, null, false));
return Result.success(flexibleGroupService.queryAllSignUsers(pageForm, groupId));
}
@At
@@ -98,16 +99,16 @@ public class RecuperationFlexibleGroupQueryController {
String searchKeyword, HttpServletResponse response) {
List<ExcelExportEntity> columns = new ArrayList<>();
columns.add(new ExcelExportEntity("序号", "no", 10));
columns.add(new ExcelExportEntity("工号", "loginName", 18));
columns.add(new ExcelExportEntity("姓名", "userName", 14));
columns.add(new ExcelExportEntity("联系电话", "mobile", 18));
columns.add(new ExcelExportEntity("身份证号", "idCard", 24));
columns.add(new ExcelExportEntity("单位", "unitName", 24));
columns.add(new ExcelExportEntity("工会", "unionName", 20));
columns.add(new ExcelExportEntity("旅行社", "travelAgencyName", 22));
columns.add(new ExcelExportEntity("工号", "loginName", 20));
columns.add(new ExcelExportEntity("姓名", "userName", 20));
columns.add(new ExcelExportEntity("联系电话", "mobile", 20));
columns.add(new ExcelExportEntity("身份证号", "idCard", 30));
columns.add(new ExcelExportEntity("单位", "unitName", 30));
columns.add(new ExcelExportEntity("工会", "unionName", 30));
columns.add(new ExcelExportEntity("旅行社", "travelAgencyName", 30));
columns.add(new ExcelExportEntity("报名时间", "signingUptime", 20));
columns.add(new ExcelExportEntity("团长", "leaderName", 20));
columns.add(new ExcelExportEntity("是否成团", "formedTeamState", 14));
columns.add(new ExcelExportEntity("团长", "leaderName", 25));
columns.add(new ExcelExportEntity("是否成团", "formedTeamState", 15));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, columns,
@@ -13,6 +13,9 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/** PC 端线路报名情况及成团通知。 */
@IocBean
@At("/platform/recuperation/lineStatistics")
@@ -48,6 +51,24 @@ public class RecuperationLineStatisticsController {
return Result.success(statisticsService.lineOptions(startYear, endYear, signUpMode, regionalNature));
}
/** lineId 为线路 IDdata 中 selectId 表示具体出行批次,times 为时间及报名人数说明。 */
@At
@SaCheckPermission("recuperation.lineStatistics")
public Result getLinePlayTimes(String lineId, Integer startYear, Integer endYear) {
return Result.success(statisticsService.linePlayTimeOptions(lineId, startYear, endYear));
}
/** 按当前筛选条件导出达到最低成团人数的线路人员,并按线路生成 Excel 压缩包。 */
@At
@Ok("void")
@SaCheckPermission("recuperation.lineStatistics")
public void doExport(Integer startYear, Integer endYear, String unionId, String takePartInLineId,
String lotId, String selectId, String regionalNature,
HttpServletResponse response) throws IOException {
statisticsService.exportFormedLineUsers(startYear, endYear, unionId, takePartInLineId,
lotId, selectId, regionalNature, response);
}
@At
@SaCheckPermission("recuperation.lineStatistics")
public Result countSuccessNotice(String id) { return Result.success(enrollService.countLineGroupNoticeUsers(id, true)); }
@@ -4,6 +4,8 @@ import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/** PC 端线路成团统计查询。 */
@@ -14,4 +16,30 @@ public interface RecuperationLineStatisticsService {
Pagination userPage(PageForm pageForm, String lineSelectId, String searchKeyword, String unionId, String unitId);
List<NutMap> lineOptions(Integer startYear, Integer endYear, String signUpMode, String regionalNature);
/**
* 查询指定线路在年度范围内的出行批次选项。
*
* @param lineId 线路 ID
* @param startYear 开始年度
* @param endYear 结束年度
* @return selectId 为工会选线记录 ID,times 为出行时间及报名人数说明
*/
List<NutMap> linePlayTimeOptions(String lineId, Integer startYear, Integer endYear);
/**
* 导出达到最低成团人数的线路报名人员。
*
* @param startYear 开始年度
* @param endYear 结束年度
* @param unionId 报名人员所属工会 ID
* @param lineId 线路 ID
* @param lotId 标段 ID
* @param selectId 工会选线记录 ID,用于限定具体出行批次
* @param regionalNature 线路类型,可传省内、省外或空值
* @param response 返回 ZIP 文件的 HTTP 响应
*/
void exportFormedLineUsers(Integer startYear, Integer endYear, String unionId, String lineId,
String lotId, String selectId, String regionalNature,
HttpServletResponse response) throws IOException;
}
@@ -105,6 +105,15 @@ public interface RecuperationProvinceFlexibleGroupService extends BaseService<Re
Pagination querySignUsers(PageForm pageForm, String groupId, String groupLeaderUserId,
String groupLeaderLoginName, Boolean noGroupLeader);
/**
* 查询一个灵活组团的全部报名人员,并按团长、团内身份和报名时间排序。
*
* @param pageForm 分页信息
* @param groupId 灵活组团 ID
* @return 报名人员分页数据
*/
Pagination queryAllSignUsers(PageForm pageForm, String groupId);
/** 查询年度区间内可用旅行社。 */
List<NutMap> queryTravelAgencyOptions(Integer startYear, Integer endYear);
@@ -1594,7 +1594,7 @@ public class RecuperationEnrollServiceImpl extends BaseServiceImpl<RecuperationE
AND rating.evaluationForAccommodation IS NOT NULL AND rating.evaluationForDining IS NOT NULL
AND rating.evaluationForTransportation IS NOT NULL THEN rating.loginName END) AS ratingCount
FROM the_rapy_recuperation_travel_agency agency
LEFT JOIN ($ratingSql) rating ON rating.travelAgencyId=agency.id
LEFT JOIN ($ratingSql) rating ON rating.travelAgencyId=agency.id AND rating.statisticYear=@year
WHERE agency.year=@year
GROUP BY agency.id, agency.travelAgencyName
ORDER BY CASE WHEN avg IS NULL THEN 1 ELSE 0 END, avg DESC, ratingCount DESC, agency.travelAgencyName
@@ -1623,7 +1623,7 @@ public class RecuperationEnrollServiceImpl extends BaseServiceImpl<RecuperationE
AND rating.evaluationForAccommodation IS NOT NULL AND rating.evaluationForDining IS NOT NULL AND rating.evaluationForTransportation IS NOT NULL THEN rating.loginName
WHEN rating.typeName IN ('灵活组团','定点') AND rating.evaluationForTravelAgency IS NOT NULL AND rating.evaluationForAccommodation IS NOT NULL THEN rating.loginName END) AS ratingCount
FROM ($ratingSql) rating
WHERE IFNULL(rating.schemeId,'')<>''
WHERE rating.statisticYear=@year AND IFNULL(rating.schemeId,'')<>''
GROUP BY rating.typeName, rating.schemeId, rating.schemeName, rating.travelAgencyName
ORDER BY FIELD(rating.typeName,'线路','灵活组团','定点'), rating.schemeName
""");
@@ -1635,12 +1635,20 @@ public class RecuperationEnrollServiceImpl extends BaseServiceImpl<RecuperationE
@Override
public List<NutMap> satisfactionFeedbackDetails(Integer year, String typeName) {
Sql sql = Sqls.create("""
SELECT rating.*,
SELECT rating.*, rating.schemeName AS projectName,
ROUND(CASE WHEN rating.typeName='线路' THEN
(rating.evaluationForTravelAgency+rating.evaluationForJourney+rating.evaluationForAccommodation+rating.evaluationForDining+rating.evaluationForTransportation)/5
ELSE (rating.evaluationForTravelAgency+rating.evaluationForAccommodation)/2 END,1) AS compositeScore
FROM ($ratingSql) rating
WHERE rating.feedbackContent IS NOT NULL AND rating.feedbackContent<>''
WHERE rating.statisticYear=@year AND (
rating.evaluationForTravelAgency IS NOT NULL
OR rating.evaluationForJourney IS NOT NULL
OR rating.evaluationForAccommodation IS NOT NULL
OR rating.evaluationForDining IS NOT NULL
OR rating.evaluationForTransportation IS NOT NULL
OR rating.evaluationForLine IS NOT NULL
OR IFNULL(rating.feedbackContent,'')<>''
)
ORDER BY rating.signingUptime DESC
""");
sql.setVar("ratingSql", satisfactionBaseSql()).setParam("year", year == null ? DateUtil.thisYear() : year);
@@ -1652,9 +1660,10 @@ public class RecuperationEnrollServiceImpl extends BaseServiceImpl<RecuperationE
/** 满意度统计与明细共用同一报名方式、方案和旅行社映射。 */
private String satisfactionBaseSql() {
return """
SELECT enroll.id, enroll.loginName, enroll.userName, enroll.unitName, enroll.signingUptime, enroll.feedbackContent,
SELECT enroll.id, enroll.loginName, enroll.userName, enroll.unitName, enroll.signingUptime,
YEAR(enroll.signingUptime) AS statisticYear, enroll.feedbackContent,
enroll.evaluationForTravelAgency, enroll.evaluationForJourney, enroll.evaluationForAccommodation,
enroll.evaluationForDining, enroll.evaluationForTransportation,
enroll.evaluationForDining, enroll.evaluationForTransportation, enroll.evaluationForLine,
CASE WHEN enroll.takePartInLineId IS NOT NULL THEN '线路' WHEN enroll.takePartInBaseManagementId IS NOT NULL THEN '定点' ELSE '灵活组团' END AS typeName,
CASE WHEN enroll.takePartInLineId IS NOT NULL THEN COALESCE(line.id,directLine.id) WHEN enroll.takePartInBaseManagementId IS NOT NULL THEN base.id ELSE COALESCE(flexible.id,enroll.takePartInTravelAgencyId) END AS schemeId,
CASE WHEN enroll.takePartInLineId IS NOT NULL THEN COALESCE(line.lineName,directLine.lineName) WHEN enroll.takePartInBaseManagementId IS NOT NULL THEN base.baseName ELSE COALESCE(flexible.groupName, directAgency.travelAgencyName) END AS schemeName,
@@ -1676,7 +1685,7 @@ public class RecuperationEnrollServiceImpl extends BaseServiceImpl<RecuperationE
AND firstFlexible.year=YEAR(enroll.signingUptime) AND firstFlexible.isDisabled=false
ORDER BY firstFlexible.sortNumber, firstFlexible.id LIMIT 1
)
WHERE enroll.isNormal=true AND enroll.isTakePartIn=true AND YEAR(enroll.signingUptime)=@year
WHERE enroll.isNormal=true AND enroll.isTakePartIn=true
""";
}
}
@@ -1,13 +1,20 @@
package com.budwk.app.zhgh.staffbenefit.recuperation.service.impl;
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.util.StrUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.staffbenefit.recuperation.common.RecuperationCommon;
import com.budwk.app.zhgh.staffbenefit.recuperation.model.RecuperationConfig;
import com.budwk.app.zhgh.staffbenefit.recuperation.model.RecuperationEnroll;
import com.budwk.app.zhgh.staffbenefit.recuperation.service.RecuperationLineStatisticsService;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
@@ -15,7 +22,21 @@ import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/** 线路统计服务,统一限定审核通过且未调出的报名记录。 */
@IocBean(args = {"refer:dao"})
@@ -26,11 +47,12 @@ public class RecuperationLineStatisticsServiceImpl extends BaseServiceImpl<Recup
public Pagination pageData(PageForm pageForm, Integer startYear, Integer endYear, String unionId,
String lineId, String lotId, String signUpMode, String selectId, String regionalNature) {
Sql sql = Sqls.create("""
SELECT line.id, line.lineName, line.regionalNature, lineu.id AS lineUId, lineu.signUpMode,
SELECT line.id, line.id AS lineId, line.lineName, line.regionalNature, lineu.id AS lineUId, lineu.signUpMode,
YEAR(lineu.selectTime) AS year,
CONCAT(DATE_FORMAT(lineu.playStartTime,'%m月%d日'),'-',DATE_FORMAT(lineu.playEndTime,'%m月%d日')) AS linePlayTime,
agency.travelAgencyName, agency.contact, agency.contactMobileNumber,
IF(lineu.signUpMode=2,'校工会',un.name) AS unionName, lot.lotName,
MAX(enroll.takePartInUnionId) AS usUnionId,
COALESCE((SELECT outsideQuota FROM the_rapy_recuperation_config LIMIT 1),0) AS minimumGroupSize,
COUNT(DISTINCT enroll.loginName) AS lineNum,
IF(COALESCE((SELECT familyInfo FROM the_rapy_recuperation_config LIMIT 1),1)=2,
@@ -94,4 +116,193 @@ public class RecuperationLineStatisticsServiceImpl extends BaseServiceImpl<Recup
sql.setCondition(cnd);
return listMap(sql);
}
@Override
public List<NutMap> linePlayTimeOptions(String lineId, Integer startYear, Integer endYear) {
Sql sql = Sqls.create("""
SELECT lineu.id AS selectId,
CONCAT(
DATE_FORMAT(lineu.playStartTime,'%m月%d日'),'-',DATE_FORMAT(lineu.playEndTime,'%m月%d日'),
'(报名人数:',
COUNT(DISTINCT enroll.loginName) +
IF(COALESCE((SELECT familyInfo FROM the_rapy_recuperation_config LIMIT 1),1)=2,
(SELECT COUNT(1)
FROM the_rapy_recuperation_enroll_companion companion
WHERE companion.trreId IN (
SELECT signed.id
FROM the_rapy_recuperation_enroll signed
WHERE signed.takePartInLineId=lineu.id
AND signed.isNormal=true
AND signed.stateId NOT IN (2715,2725,2735)
)),
COALESCE(SUM(enroll.familyNumber),0)),
',其中家属:',
IF(COALESCE((SELECT familyInfo FROM the_rapy_recuperation_config LIMIT 1),1)=2,
(SELECT COUNT(1)
FROM the_rapy_recuperation_enroll_companion companion
WHERE companion.trreId IN (
SELECT signed.id
FROM the_rapy_recuperation_enroll signed
WHERE signed.takePartInLineId=lineu.id
AND signed.isNormal=true
AND signed.stateId NOT IN (2715,2725,2735)
)),
COALESCE(SUM(enroll.familyNumber),0)),
'人)'
) AS times
FROM the_rapy_recuperation_line_union_select lineu
LEFT JOIN the_rapy_recuperation_enroll enroll
ON enroll.takePartInLineId=lineu.id
AND enroll.isNormal=true
AND enroll.stateId NOT IN (2715,2725,2735)
$condition
""");
Cnd cnd = Cnd.where("lineu.lineId", "=", lineId)
.andEX("YEAR(lineu.selectTime)", ">=", startYear)
.andEX("YEAR(lineu.selectTime)", "<=", endYear);
if (!AuthUtil.hasRoleOr("sysadmin", "H06", "A06")) {
cnd.and("lineu.unionId", "=", SecurityUtil.getUnionId());
}
cnd.groupBy("lineu.id", "lineu.playStartTime", "lineu.playEndTime");
cnd.asc("lineu.playStartTime");
sql.setCondition(cnd);
return listMap(sql);
}
@Override
public void exportFormedLineUsers(Integer startYear, Integer endYear, String unionId, String lineId,
String lotId, String selectId, String regionalNature,
HttpServletResponse response) throws IOException {
List<NutMap> exportRows = queryLineExportRows(startYear, endYear, unionId, lineId, lotId, selectId, regionalNature);
RecuperationConfig config = dao().fetch(RecuperationConfig.class, Cnd.NEW());
Integer minimumGroupSize = config == null ? null : config.getOutsideQuota();
Map<String, List<NutMap>> batchGroups = exportRows.stream().collect(Collectors.groupingBy(
row -> StrUtil.blankToDefault(row.getString("takePartInLineId"), ""),
LinkedHashMap::new,
Collectors.toList()));
List<NutMap> formedRows = batchGroups.values().stream()
.filter(rows -> isFormedGroup(rows, minimumGroupSize))
.flatMap(List::stream)
.collect(Collectors.toList());
Map<String, List<NutMap>> lineGroups = formedRows.stream().collect(Collectors.groupingBy(
row -> StrUtil.blankToDefault(row.getString("lineId"), ""),
LinkedHashMap::new,
Collectors.toList()));
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment;filename="
+ URLEncoder.encode("疗休养集体线路报名人员名单.zip", StandardCharsets.UTF_8));
Set<String> zipEntryNames = new HashSet<>();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(response.getOutputStream()))) {
for (List<NutMap> rows : lineGroups.values()) {
String lineName = rows.isEmpty() ? "未命名" : StrUtil.blankToDefault(rows.get(0).getString("lineName"), "未命名");
writeExcelToZip(zipOutputStream, zipEntryNames, lineName, rows);
}
zipOutputStream.flush();
}
response.flushBuffer();
}
/** 查询导出所需的审核通过线路报名人员,并按当前登录角色限制可见工会。 */
private List<NutMap> queryLineExportRows(Integer startYear, Integer endYear, String unionId, String lineId,
String lotId, String selectId, String regionalNature) {
Sql sql = Sqls.create("""
SELECT line.id AS lineId, line.lineName, line.regionalNature, agency.travelAgencyName,
enroll.*, DATE_FORMAT(lineu.playStartTime,'%Y-%m-%d') AS playStartTime,
IF(COALESCE((SELECT familyInfo FROM the_rapy_recuperation_config LIMIT 1),1)=2,
(SELECT COUNT(1) FROM the_rapy_recuperation_enroll_companion companion WHERE companion.trreId=enroll.id),
COALESCE(enroll.familyNumber,0)) AS familyCount,
(SELECT GROUP_CONCAT(
companion.userName,'-',companion.sex,'',companion.idcard,'、',companion.mobile,'-',
IFNULL(bed.bedType,''),',家属备注:',IFNULL(companion.remark,'')
SEPARATOR '')
FROM the_rapy_recuperation_enroll_companion companion
LEFT JOIN the_rapy_recuperation_enroll_bed bed ON bed.id=companion.bedInfoId
WHERE companion.trreId=enroll.id) AS companionInfo,
user_info.arrivalAtSchoolDate AS schoolTime
FROM the_rapy_recuperation_enroll enroll
LEFT JOIN sys_user user_info ON user_info.loginname=enroll.loginName
LEFT JOIN the_rapy_recuperation_line_union_select lineu ON lineu.id=enroll.takePartInLineId
LEFT JOIN the_rapy_recuperation_line line ON line.id=lineu.lineId
LEFT JOIN the_rapy_recuperation_travel_agency agency ON agency.id=lineu.travelAgencyId
$condition
""");
Cnd cnd = Cnd.where("enroll.stateId", "=", 2750)
.and("enroll.isNormal", "=", true)
.and("enroll.takePartInLineId", "is not", null)
.and("enroll.takePartInLineId", "!=", "")
.andEX("YEAR(enroll.signingUptime)", ">=", startYear)
.andEX("YEAR(enroll.signingUptime)", "<=", endYear)
.andEX("line.id", "=", lineId)
.andEX("line.lotId", "=", lotId)
.andEX("lineu.id", "=", selectId)
.andEX("line.regionalNature", "=", regionalNature);
if (AuthUtil.hasRoleOr("sysadmin", "H06", "A06")) {
cnd.andEX("enroll.selfUnionId", "=", unionId);
} else {
cnd.and(Cnd.exps("enroll.takePartInUnionId", "=", SecurityUtil.getUnionId())
.or("enroll.selfUnionId", "=", SecurityUtil.getUnionId()));
}
cnd.asc("line.lineName").asc("lineu.playStartTime").asc("enroll.unionName");
sql.setCondition(cnd);
List<NutMap> rows = listMap(sql);
rows.forEach(row -> row.setv("remark", RecuperationCommon.getRemarkBySchoolTime(row.getString("schoolTime"))));
return rows;
}
/** 最低成团人数同时计算去重后的教职工人数和家属人数。 */
private boolean isFormedGroup(List<NutMap> rows, Integer minimumGroupSize) {
if (minimumGroupSize == null || minimumGroupSize <= 0) {
return false;
}
long teacherCount = rows.stream().map(row -> row.getString("loginName"))
.filter(StrUtil::isNotBlank).distinct().count();
int familyCount = rows.stream().mapToInt(row -> row.getInt("familyCount", 0)).sum();
return teacherCount + familyCount >= minimumGroupSize;
}
/** 每条线路生成一个 XSSF 工作簿后写入 ZIP,避免工作簿内部流与外层 ZIP 冲突。 */
private void writeExcelToZip(ZipOutputStream zipOutputStream, Set<String> zipEntryNames,
String rawLineName, List<NutMap> rows) throws IOException {
String safeLineName = rawLineName.replaceAll("[\\\\/:*?\"<>|]", "_").replaceAll("[\\r\\n]", "").trim();
String entryName = "集体线路报名人员/" + StrUtil.blankToDefault(safeLineName, "未命名") + ".xlsx";
int index = 1;
while (!zipEntryNames.add(entryName)) {
entryName = "集体线路报名人员/" + StrUtil.blankToDefault(safeLineName, "未命名") + "-" + index++ + ".xlsx";
}
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
try (Workbook workbook = ExcelExportUtil.exportExcel(exportParams, lineExportColumns(), rows);
ByteArrayOutputStream excelOutputStream = new ByteArrayOutputStream()) {
workbook.write(excelOutputStream);
zipOutputStream.putNextEntry(new ZipEntry(entryName));
zipOutputStream.write(excelOutputStream.toByteArray());
zipOutputStream.closeEntry();
}
}
/** 旧版成团线路人员导出的列结构。 */
private List<ExcelExportEntity> lineExportColumns() {
List<ExcelExportEntity> columns = new ArrayList<>();
columns.add(new ExcelExportEntity("姓名", "userName", 20));
columns.add(new ExcelExportEntity("工号", "loginName", 20));
columns.add(new ExcelExportEntity("性别", "sex", 10));
columns.add(new ExcelExportEntity("所属单位", "unitName", 20));
columns.add(new ExcelExportEntity("所属工会", "unionName", 20));
columns.add(new ExcelExportEntity("身份证号", "idCard", 30));
columns.add(new ExcelExportEntity("手机号", "mobile", 15));
columns.add(new ExcelExportEntity("备注", "remark", 30));
ExcelExportEntity signingTime = new ExcelExportEntity("报名时间", "signingUptime", 20);
signingTime.setFormat("yyyy-MM-dd HH:mm:ss");
columns.add(signingTime);
columns.add(new ExcelExportEntity("出行时间", "playStartTime", 20));
columns.add(new ExcelExportEntity("线路名称", "lineName", 20));
columns.add(new ExcelExportEntity("旅行社", "travelAgencyName", 20));
columns.add(new ExcelExportEntity("家属人数", "familyCount", 12));
ExcelExportEntity companionInfo = new ExcelExportEntity("家属信息", "companionInfo", 50);
companionInfo.setWrap(true);
columns.add(companionInfo);
return columns;
}
}
@@ -380,6 +380,9 @@ public class RecuperationProvinceFlexibleGroupServiceImpl extends BaseServiceImp
@Override
public Pagination queryPage(PageForm pageForm, Integer startYear, Integer endYear, String groupName,
String travelAgencyId, String searchKeyword) {
if (StrUtil.isNotBlank(searchKeyword)) {
return queryPersonMatchedGroups(pageForm, startYear, endYear, groupName, travelAgencyId, searchKeyword);
}
Sql sql = Sqls.create("""
SELECT fg.id, fg.groupName, fg.year, ta.travelAgencyName,
COUNT(DISTINCT CASE WHEN enroll.groupLeaderUserId IS NOT NULL AND enroll.groupLeaderUserId <> ''
@@ -412,6 +415,53 @@ public class RecuperationProvinceFlexibleGroupServiceImpl extends BaseServiceImp
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
/** 姓名或工号查询时按匹配人员展开,并保留组团总团队数和总报名人数。 */
private Pagination queryPersonMatchedGroups(PageForm pageForm, Integer startYear, Integer endYear,
String groupName, String travelAgencyId, String searchKeyword) {
Sql sql = Sqls.create("""
SELECT fg.id, CONCAT(fg.id,'_',enroll.id) AS rowKey, fg.groupName, fg.year,
ta.travelAgencyName, enroll.userName AS signUserName, enroll.loginName AS signLoginName,
(SELECT COUNT(DISTINCT CASE
WHEN grouped.groupLeaderUserId IS NOT NULL AND grouped.groupLeaderUserId<>'' THEN grouped.groupLeaderUserId
WHEN grouped.groupLeaderLoginName IS NOT NULL AND grouped.groupLeaderLoginName<>'' THEN grouped.groupLeaderLoginName
END)
FROM the_rapy_recuperation_enroll grouped
WHERE grouped.takePartInTravelAgencyId=fg.travelAgencyId
AND YEAR(grouped.signingUptime)=fg.year
AND grouped.isNormal=true
AND grouped.stateId NOT IN ($auditFailStates)) AS groupCount,
(SELECT COUNT(DISTINCT grouped.loginName)
FROM the_rapy_recuperation_enroll grouped
WHERE grouped.takePartInTravelAgencyId=fg.travelAgencyId
AND YEAR(grouped.signingUptime)=fg.year
AND grouped.isNormal=true
AND grouped.stateId NOT IN ($auditFailStates)) AS signCount
FROM the_rapy_recuperation_province_flexible_group fg
LEFT JOIN the_rapy_recuperation_travel_agency ta ON ta.id=fg.travelAgencyId
LEFT JOIN the_rapy_recuperation_enroll enroll
ON enroll.takePartInTravelAgencyId=fg.travelAgencyId
AND YEAR(enroll.signingUptime)=fg.year
AND enroll.isNormal=true
AND enroll.stateId NOT IN ($auditFailStates)
$condition
""");
Cnd cnd = Cnd.where("ta.signUpTravelAgency", "=", true)
.andEX("fg.year", ">=", startYear)
.andEX("fg.year", "<=", endYear)
.andEX("fg.travelAgencyId", "=", travelAgencyId)
.and(Cnd.likeEX("fg.groupName", groupName))
.and(Cnd.exps("enroll.userName", "like", "%" + searchKeyword + "%")
.or("enroll.loginName", "like", "%" + searchKeyword + "%"));
if (!AuthUtil.hasRoleOr("sysadmin", "A06") && AuthUtil.hasRoleOr("H04")) {
cnd.and("fg.createUnionId", "=", SecurityUtil.getUnionId());
}
cnd.groupBy("fg.id", "enroll.id");
cnd.desc("fg.year").asc("fg.groupName").asc("enroll.userName");
sql.setVar("auditFailStates", auditFailStates());
sql.setCondition(cnd);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
public List<NutMap> queryGroupInfo(String groupId) {
Sql sql = Sqls.create("""
@@ -462,11 +512,44 @@ public class RecuperationProvinceFlexibleGroupServiceImpl extends BaseServiceImp
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
public Pagination queryAllSignUsers(PageForm pageForm, String groupId) {
Sql sql = Sqls.create("""
SELECT enroll.id, enroll.loginName, enroll.userName,
CASE WHEN enroll.groupLeaderLoginName IS NOT NULL
AND enroll.groupLeaderLoginName<>''
AND enroll.loginName=enroll.groupLeaderLoginName
THEN '团长' ELSE '队员' END AS identity,
enroll.unionName, enroll.unitName, enroll.sex, enroll.mobile,
(SELECT COUNT(1) FROM the_rapy_recuperation_enroll_companion WHERE trreId=enroll.id) AS isFamily
FROM the_rapy_recuperation_enroll enroll
WHERE enroll.takePartInTravelAgencyId=(
SELECT travelAgencyId FROM the_rapy_recuperation_province_flexible_group WHERE id=@groupId)
AND YEAR(enroll.signingUptime)=(
SELECT year FROM the_rapy_recuperation_province_flexible_group WHERE id=@groupId)
AND enroll.isNormal=true
AND enroll.stateId NOT IN ($auditFailStates)
ORDER BY enroll.groupLeaderUserName,
CASE WHEN enroll.groupLeaderLoginName IS NOT NULL
AND enroll.groupLeaderLoginName<>''
AND enroll.loginName=enroll.groupLeaderLoginName
THEN 0 ELSE 1 END,
enroll.signingUptime
""");
sql.setParam("groupId", groupId).setVar("auditFailStates", auditFailStates());
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
/** 过滤浏览器可能传入的 undefined/null 文本,避免错误命中团长条件。 */
private boolean isValidGroupLeaderParam(String value) {
return StrUtil.isNotBlank(value) && !"undefined".equalsIgnoreCase(value) && !"null".equalsIgnoreCase(value);
}
/** 灵活组团查询排除分工会、线路工会和校工会审核失败状态。 */
private String auditFailStates() {
return RecuperationState.UNITFAIL + "," + RecuperationState.LINEUNITFAIL + "," + RecuperationState.SCHOOLFAIL;
}
@Override
public List<NutMap> queryTravelAgencyOptions(Integer startYear, Integer endYear) {
Sql sql = Sqls.create("""
@@ -498,24 +581,52 @@ public class RecuperationProvinceFlexibleGroupServiceImpl extends BaseServiceImp
Sql sql = Sqls.create("""
SELECT enroll.loginName, enroll.userName, enroll.mobile, enroll.idCard, enroll.unitName, enroll.unionName,
ta.travelAgencyName, DATE_FORMAT(enroll.signingUptime,'%Y-%m-%d %H:%i:%s') AS signingUptime,
CONCAT(IFNULL(enroll.groupLeaderUserName,''), IF(enroll.groupLeaderLoginName IS NULL OR enroll.groupLeaderLoginName='', '', CONCAT('',enroll.groupLeaderLoginName,''))) AS leaderName,
CASE WHEN (SELECT COUNT(DISTINCT sameLeader.loginName) FROM the_rapy_recuperation_enroll sameLeader
WHERE sameLeader.takePartInTravelAgencyId=enroll.takePartInTravelAgencyId
AND sameLeader.groupLeaderLoginName=enroll.groupLeaderLoginName AND sameLeader.isNormal=true
AND YEAR(sameLeader.signingUptime)=YEAR(enroll.signingUptime))>=3 THEN '已成团' ELSE '未成团' END AS formedTeamState
CASE
WHEN (enroll.groupLeaderUserName IS NULL OR enroll.groupLeaderUserName='')
AND (enroll.groupLeaderLoginName IS NULL OR enroll.groupLeaderLoginName='') THEN '未选择'
WHEN enroll.groupLeaderLoginName IS NULL OR enroll.groupLeaderLoginName='' THEN enroll.groupLeaderUserName
WHEN enroll.groupLeaderUserName IS NULL OR enroll.groupLeaderUserName='' THEN enroll.groupLeaderLoginName
ELSE CONCAT(enroll.groupLeaderUserName,'',enroll.groupLeaderLoginName,'')
END AS leaderName,
CASE WHEN (
SELECT COUNT(DISTINCT sameLeader.loginName)
FROM the_rapy_recuperation_enroll sameLeader
WHERE sameLeader.takePartInTravelAgencyId=enroll.takePartInTravelAgencyId
AND ((enroll.groupLeaderUserId IS NOT NULL AND enroll.groupLeaderUserId<>''
AND sameLeader.groupLeaderUserId=enroll.groupLeaderUserId)
OR (enroll.groupLeaderLoginName IS NOT NULL AND enroll.groupLeaderLoginName<>''
AND sameLeader.groupLeaderLoginName=enroll.groupLeaderLoginName))
AND YEAR(sameLeader.signingUptime)=YEAR(enroll.signingUptime)
AND sameLeader.isNormal=true
AND sameLeader.stateId NOT IN ($auditFailStates)
)>=3 THEN '已成团' ELSE '未成团' END AS formedTeamState
FROM the_rapy_recuperation_enroll enroll
LEFT JOIN the_rapy_recuperation_travel_agency ta ON ta.id=enroll.takePartInTravelAgencyId
LEFT JOIN the_rapy_recuperation_province_flexible_group fg ON fg.travelAgencyId=enroll.takePartInTravelAgencyId AND fg.year=YEAR(enroll.signingUptime)
LEFT JOIN the_rapy_recuperation_province_flexible_group fg ON fg.id=(
SELECT configured.id
FROM the_rapy_recuperation_province_flexible_group configured
WHERE configured.travelAgencyId=enroll.takePartInTravelAgencyId
AND configured.year=YEAR(enroll.signingUptime)
AND configured.isDisabled=false
ORDER BY configured.id
LIMIT 1)
$condition
""");
Cnd cnd = Cnd.where("fg.year", ">=", startYear).and("fg.year", "<=", endYear)
.and("enroll.isNormal", "=", true);
Cnd cnd = Cnd.where("ta.signUpTravelAgency", "=", true)
.and("fg.id", "is not", null)
.andEX("fg.year", ">=", startYear)
.andEX("fg.year", "<=", endYear)
.and("enroll.isNormal", "=", true)
.and("enroll.stateId", "not in", Lang.array(RecuperationState.UNITFAIL,
RecuperationState.LINEUNITFAIL, RecuperationState.SCHOOLFAIL));
cnd.andEX("fg.travelAgencyId", "=", travelAgencyId).and(Cnd.likeEX("fg.groupName", groupName));
if (StrUtil.isNotBlank(searchKeyword)) {
cnd.and(Cnd.exps("enroll.userName", "like", "%" + searchKeyword + "%").or("enroll.loginName", "like", "%" + searchKeyword + "%"));
}
if (!AuthUtil.hasRoleOr("sysadmin", "A06") && AuthUtil.hasRoleOr("H04")) cnd.and("fg.createUnionId", "=", SecurityUtil.getUnionId());
cnd.asc("fg.year").asc("ta.serialNumber * 1").asc("enroll.groupLeaderUserName").asc("enroll.signingUptime");
cnd.asc("ta.serialNumber * 1").asc("ta.travelAgencyName").asc("enroll.groupLeaderUserName")
.asc("enroll.groupLeaderLoginName").asc("fg.year").asc("enroll.signingUptime");
sql.setVar("auditFailStates", auditFailStates());
sql.setCondition(cnd);
List<NutMap> list = listMap(sql);
for (int i = 0; i < list.size(); i++) list.get(i).setv("no", i + 1);
@@ -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("此操作将永久删除, 是否继续?", "提示", {
@@ -1,43 +1,289 @@
<!--# layout("/layouts/platform.html"){ #-->
<style>
.query-row {
height: 70px;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.query-row .el-col { overflow: hidden; }
.query-row:not(:last-child) { border-bottom: 1px dashed rgb(230, 230, 230); }
.query-row-title { width: 120px; }
.el-table-container { padding-top: 0; }
</style>
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="年度范围"><el-date-picker v-model="yearRange" type="yearrange" value-format="yyyy" range-separator="至" start-placeholder="开始年度" end-placeholder="结束年度" @change="yearChange"></el-date-picker></search-item>
<search-item label="灵活组团"><el-select v-model="pageForm.groupName" clearable filterable @change="doSearch"><el-option v-for="item in groupOptions" :key="item.id" :label="item.groupName" :value="item.groupName"></el-option></el-select></search-item>
<search-item label="旅行社"><el-select v-model="pageForm.travelAgencyId" clearable filterable @change="doSearch"><el-option v-for="item in agencyOptions" :key="item.id" :label="item.travelAgencyName" :value="item.id"></el-option></el-select></search-item>
<search-item label="姓名/工号"><el-input v-model="pageForm.searchKeyword" clearable @keyup.enter.native="doSearch"></el-input></search-item>
</search>
<el-row align="middle" class="query-row" type="flex">
<el-col class="query-row-title"></el-col>
<el-col class="query-row-content">
<el-row><el-col :span="12">
<span>&emsp;&emsp;度:</span>
<el-date-picker :clearable="false" v-model="pageForm.startYear" type="year" value-format="yyyy" placeholder="选择年" style="width: 38%" @change="yearChange"></el-date-picker>
<span></span>
<el-date-picker :clearable="false" v-model="pageForm.endYear" type="year" value-format="yyyy" placeholder="请选择年" style="width: 38%" @change="yearChange"></el-date-picker>
</el-col></el-row>
</el-col>
</el-row>
<el-row align="middle" class="query-row" type="flex">
<el-col class="query-row-title"></el-col>
<el-col class="query-row-content">
<el-row>
<el-col :span="12">
<span>组团名称:</span>
<el-select v-model="pageForm.groupName" placeholder="请选择组团名称" filterable clearable style="width: 80%" @change="doSearch">
<el-option v-for="item in flexibleGroupList" :key="item.id" :label="item.groupName" :value="item.groupName"></el-option>
</el-select>
</el-col>
<el-col :span="12">
<span>&ensp;&ensp;社:</span>
<el-select v-model="pageForm.travelAgencyId" placeholder="请选择旅行社" filterable clearable style="width: 80%" @change="doSearch">
<el-option v-for="item in travelAgencyList" :key="item.id" :label="item.travelAgencyName + (item.serialNumber ? '(' + item.serialNumber + ')' : '')" :value="item.id"></el-option>
</el-select>
</el-col>
</el-row>
</el-col>
</el-row>
<el-row align="middle" class="query-row" type="flex">
<el-col class="query-row-title"></el-col>
<el-col class="query-row-content">
<el-row>
<el-col :span="12">
<span>姓名/工号:</span>
<el-input :value="pageForm.searchKeyword" clearable placeholder="请输入姓名或工号" style="width: 80%" @input="personKeywordChange" @clear="clearPersonKeyword" @keyup.enter.native="doSearch"></el-input>
</el-col>
<el-col :span="12">
<el-button icon="el-icon-search" size="small" type="primary" @click="doSearch">查询</el-button>
<el-button icon="el-icon-refresh-left" size="small" @click="clearPersonKeyword">重置</el-button>
</el-col>
</el-row>
</el-col>
</el-row>
</el-card>
<el-card shadow="never" class="mt20">
<table-tool label="省内灵活组团查询"><template slot="right"><el-button type="success" size="small" @click="doExport">导出Excel</el-button></template></table-tool>
<el-table v-loading="tableLoading" :data="tableData" :size="tableSize">
<el-table-column type="index" :index="indexMethod" label="序号" width="60"></el-table-column><el-table-column prop="year" label="年度" width="90"></el-table-column><el-table-column prop="groupName" label="组团名称" min-width="180"></el-table-column><el-table-column prop="travelAgencyName" label="旅行社" min-width="180"></el-table-column><el-table-column prop="groupCount" label="团队数" width="90"></el-table-column><el-table-column prop="signCount" label="报名人数" width="100"></el-table-column><el-table-column label="操作" width="180"><template v-slot="{row}"><el-button size="mini" @click="openGroups(row)">查看团队</el-button><el-button type="primary" size="mini" @click="openAllUsers(row)">全部人员</el-button></template></el-table-column>
<el-card shadow="never" class="mt10">
<table-tool label="灵活组团查询" :app="this" ref="table_tool">
<el-button icon="el-icon-s-promotion" size="small" type="primary" @click="doExport">导出Excel</el-button>
</table-tool>
<el-table :data="tableData" :row-key="tableRowKey" style="width: 100%" ref="table" v-loading="tableLoading" @sort-change="pageOrder">
<el-table-column align="center" header-align="center" type="index" label="序号" width="80" key="#index">
<template v-slot="scope"><span>{{scope.$index + (pageForm.pageNumber - 1) * pageForm.pageSize + 1}}</span></template>
</el-table-column>
<el-table-column v-for="column in tableColumns" v-if="!column.personOnly || pageForm.searchKeyword" :key="column.prop" :label="column.label" :prop="column.prop" :width="column.width" :sortable="column.sortable" align="center" header-align="center" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="200">
<template v-slot="{row}"><el-button @click="openView(row)" size="mini" type="primary">查看团</el-button><el-button @click="openViewUser(row)" size="mini" type="primary">查看人员</el-button></template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="团队列表" :visible.sync="groupVisible" width="900px" append-to-body>
<el-table v-loading="dialogLoading" :data="groups"><el-table-column prop="groupLeaderUserName" label="团长姓名"></el-table-column><el-table-column prop="groupLeaderLoginName" label="团长工号"></el-table-column><el-table-column prop="groupLeaderPassword" label="团口令"></el-table-column><el-table-column prop="userCount" label="人数" width="90"></el-table-column><el-table-column label="操作" width="100"><template v-slot="{row}"><el-button type="primary" size="mini" @click="openGroupUsers(row)">人员</el-button></template></el-table-column></el-table>
</template>
</guava>
<el-dialog :visible.sync="userVisible" :close-on-click-modal="false" top="2%" title="报名人员" width="65%">
<el-table :data="signUserData" style="width: 100%" ref="userTable" v-loading="userLoading">
<el-table-column align="center" header-align="center" type="index" label="序号" width="80"></el-table-column>
<el-table-column prop="userName" label="姓名" header-align="center" align="center"></el-table-column>
<el-table-column prop="loginName" label="工号" header-align="center" align="center"></el-table-column>
<el-table-column prop="unionName" label="所属工会" header-align="center" align="center"></el-table-column>
<el-table-column prop="unitName" label="单位" header-align="center" align="center"></el-table-column>
<el-table-column prop="sex" label="性别" header-align="center" align="center"></el-table-column>
<el-table-column prop="mobile" label="手机号" header-align="center" align="center" width="120"></el-table-column>
<el-table-column prop="identity" label="身份" header-align="center" align="center"></el-table-column>
<el-table-column prop="isFamily" label="是否有家属" header-align="center" align="center"><template v-slot="{row}"><span v-if="row.isFamily > 0"></span><span v-else></span></template></el-table-column>
<el-table-column label="操作" width="100"><template v-slot="{row}"><el-button @click="deleteUser(row)" size="mini" type="danger">删除</el-button></template></el-table-column>
</el-table>
<span slot="footer" class="dialog-footer"><el-button @click="closeUserDialog" type="primary">取 消</el-button></span>
</el-dialog>
<el-dialog title="报名人员" :visible.sync="userVisible" width="1050px" append-to-body>
<el-table v-loading="userLoading" :data="users"><el-table-column type="index" label="序号" width="60"></el-table-column><el-table-column prop="loginName" label="工号" width="110"></el-table-column><el-table-column prop="userName" label="姓名" width="100"></el-table-column><el-table-column prop="identity" label="身份" width="80"></el-table-column><el-table-column prop="sex" label="性别" width="60"></el-table-column><el-table-column prop="unitName" label="单位" show-overflow-tooltip></el-table-column><el-table-column prop="unionName" label="工会" show-overflow-tooltip></el-table-column><el-table-column prop="mobile" label="联系电话" width="130"></el-table-column><el-table-column prop="isFamily" label="家属人数" width="90"></el-table-column><el-table-column label="操作" width="80"><template v-slot="{row}"><el-button type="danger" size="mini" @click="deleteUser(row)">删除</el-button></template></el-table-column></el-table>
<el-pagination class="mt20" background layout="total, sizes, prev, pager, next" :current-page="userPage.pageNumber" :page-size="userPage.pageSize" :total="userPage.totalCount" @current-change="userPageChange" @size-change="userSizeChange"></el-pagination>
<el-dialog :visible.sync="groupVisible" :close-on-click-modal="false" top="2%" title="团信息">
<el-table :data="groupTableData" max-height="600" v-loading="dialogLoading">
<el-table-column label="序号" type="index" header-align="center" align="center" width="50"></el-table-column>
<el-table-column prop="groupLeaderUserName" label="团长" header-align="center" align="center" sortable><template v-slot="{row}"><span v-if="row.groupLeaderUserName">{{row.groupLeaderUserName}}{{row.groupLeaderLoginName}}</span><span v-else>未选择</span></template></el-table-column>
<el-table-column prop="groupLeaderPassword" label="口令" header-align="center" align="center"><template v-slot="{row}"><span>{{row.groupLeaderPassword || '-'}}</span></template></el-table-column>
<el-table-column prop="userCount" label="报名人数" header-align="center" align="center" sortable></el-table-column>
<el-table-column label="操作" width="150"><template v-slot="{row}"><el-button @click="openViewUserByTime(row)" size="mini" type="primary">查看人员</el-button></template></el-table-column>
</el-table>
<span slot="footer" class="dialog-footer"><el-button @click="closeGroupDialog" type="primary">取 消</el-button></span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el:'#app',mixins:[initTableMixins],data(){const year=new Date().getFullYear().toString();return{yearRange:[year,year],pageForm:{pageNumber:1,pageSize:10,totalCount:0,startYear:year,endYear:year,groupName:'',travelAgencyId:'',searchKeyword:''},agencyOptions:[],groupOptions:[],currentGroup:{},groups:[],users:[],groupVisible:false,userVisible:false,dialogLoading:false,userLoading:false,userPage:{pageNumber:1,pageSize:10,totalCount:0},userQuery:{}}},
methods:{
pageData(){this.$set(this,'tableLoading',true);this.$axios.post(loc()+'/pageData',this.pageForm).then((res)=>{if(res.code===0){this.$set(this,'tableData',res.data.list||[]);this.$set(this.pageForm,'totalCount',res.data.totalCount||0)}}).finally(()=>{this.$set(this,'tableLoading',false)})},
yearChange(value){this.$set(this.pageForm,'startYear',value&&value.length?value[0]:'');this.$set(this.pageForm,'endYear',value&&value.length?value[1]:'');this.loadOptions();this.doSearch()},
loadOptions(){const params={startYear:this.pageForm.startYear,endYear:this.pageForm.endYear};this.$axios.post(loc()+'/selectTravelAgencyList',params).then((res)=>{if(res.code===0)this.$set(this,'agencyOptions',res.data||[])});this.$axios.post(loc()+'/selectFlexibleGroupList',params).then((res)=>{if(res.code===0)this.$set(this,'groupOptions',res.data||[])})},
doExport(){window.open(loc()+'/doExport?'+$.param(this.pageForm))},
openGroups(row){this.$set(this,'currentGroup',row);this.$set(this,'groupVisible',true);this.$set(this,'dialogLoading',true);this.$axios.post(loc()+'/getGroupInfo',{groupId:row.id}).then((res)=>{if(res.code===0)this.$set(this,'groups',res.data||[])}).finally(()=>{this.$set(this,'dialogLoading',false)})},
openAllUsers(row){this.$set(this,'currentGroup',row);this.$set(this,'userQuery',{});this.$set(this.userPage,'pageNumber',1);this.$set(this,'userVisible',true);this.loadUsers()},
openGroupUsers(row){this.$set(this,'userQuery',{groupLeaderUserId:row.groupLeaderUserId,groupLeaderLoginName:row.groupLeaderLoginName,noGroupLeader:!row.groupLeaderUserId&&!row.groupLeaderLoginName});this.$set(this.userPage,'pageNumber',1);this.$set(this,'userVisible',true);this.loadUsers()},
loadUsers(){this.$set(this,'userLoading',true);const params=Object.assign({groupId:this.currentGroup.id,pageNumber:this.userPage.pageNumber,pageSize:this.userPage.pageSize},this.userQuery);this.$axios.post(loc()+'/getSignUser',params).then((res)=>{if(res.code===0){this.$set(this,'users',res.data.list||[]);this.$set(this.userPage,'totalCount',res.data.totalCount||0)}}).finally(()=>{this.$set(this,'userLoading',false)})},
userPageChange(value){this.$set(this.userPage,'pageNumber',value);this.loadUsers()},userSizeChange(value){this.$set(this.userPage,'pageSize',value);this.$set(this.userPage,'pageNumber',1);this.loadUsers()},
deleteUser(row){this.$confirm('确定删除该报名人员吗?','提示',{type:'warning'}).then(()=>{this.$axios.post(loc()+'/deleteJoinUser',{id:row.id}).then((res)=>{if(res.code===0){this.$message.success(res.msg);this.loadUsers();this.pageData()}else this.$message.warning(res.msg)})}).catch(()=>{})}
},created(){this.loadOptions();this.pageData()}
})
const vue = new Vue({
el: '#app',
mixins: [initTableMixins],
data() {
const currentYear = moment().format('YYYY')
return {
groupVisible: false,
userVisible: false,
userLoading: false,
dialogLoading: false,
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
startYear: currentYear,
endYear: currentYear,
groupName: '',
travelAgencyId: '',
searchKeyword: ''
},
tableColumns: [
{prop: 'year', label: '年度', width: 60},
{prop: 'groupName', label: '组团名称', sortable: true},
{prop: 'travelAgencyName', label: '旅行社', sortable: true},
{prop: 'groupCount', label: '团数量'},
{prop: 'signCount', label: '报名人数'},
{prop: 'signUserName', label: '报名人员', personOnly: true},
{prop: 'signLoginName', label: '工号', personOnly: true}
],
travelAgencyList: [],
flexibleGroupList: [],
groupTableData: [],
signUserData: [],
currentGroupId: ''
}
},
methods: {
pageData() {
this.$set(this, 'tableLoading', true)
this.$axios.post(loc() + '/pageData', this.pageForm)
.then((res) => {
if (res.code === 0) {
this.$set(this, 'tableData', res.data.list || [])
this.$set(this.pageForm, 'totalCount', res.data.totalCount || 0)
}
})
.finally(() => {
this.$set(this, 'tableLoading', false)
})
},
tableRowKey(row) {
return row.rowKey || row.id
},
closeUserDialog() {
this.$set(this, 'userVisible', false)
},
closeGroupDialog() {
this.$set(this, 'groupVisible', false)
},
personKeywordChange(value) {
this.$set(this.pageForm, 'searchKeyword', value)
},
clearPersonKeyword() {
this.$set(this.pageForm, 'searchKeyword', '')
this.doSearch()
},
yearChange() {
this.getTravelAgency()
this.getFlexibleGroupList()
this.doSearch()
},
getTravelAgency() {
const params = {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear
}
this.$axios.post(loc() + '/selectTravelAgencyList', params)
.then((res) => {
if (res.code === 0) {
this.$set(this, 'travelAgencyList', res.data || [])
}
})
},
getFlexibleGroupList() {
const params = {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear
}
this.$axios.post(loc() + '/selectFlexibleGroupList', params)
.then((res) => {
if (res.code === 0) {
this.$set(this, 'flexibleGroupList', res.data || [])
}
})
},
doExport() {
window.open(loc() + '/doExport?' + $.param(this.pageForm))
},
openView(row) {
this.$set(this, 'currentGroupId', row.id)
this.$set(this, 'dialogLoading', true)
this.$axios.post(loc() + '/getGroupInfo', {groupId: row.id})
.then((res) => {
if (res.code === 0) {
this.$set(this, 'groupTableData', res.data || [])
this.$set(this, 'groupVisible', true)
}
})
.finally(() => {
this.$set(this, 'dialogLoading', false)
})
},
openViewUser(row) {
this.$set(this, 'currentGroupId', row.id)
this.loadAllSignUser()
},
openViewUserByTime(row) {
this.loadSignUserByTime(row.groupLeaderUserId, row.groupLeaderLoginName, !row.groupLeaderUserId && !row.groupLeaderLoginName)
},
loadAllSignUser() {
// groupId 传灵活组团配置主键,接口返回该组团下全部报名人员的分页结构。
this.$set(this, 'userLoading', true)
this.$axios.post(loc() + '/getAllSignUser', {
groupId: this.currentGroupId,
pageNumber: 1,
pageSize: 1000
}).then((res) => {
if (res.code === 0) {
this.$set(this, 'signUserData', res.data.list || [])
this.$set(this, 'userVisible', true)
}
}).finally(() => {
this.$set(this, 'userLoading', false)
})
},
loadSignUserByTime(groupLeaderUserId, groupLeaderLoginName, noGroupLeader) {
// 参数依次标识团长用户、团长工号和未选团长分组,接口返回对应团内人员分页结构。
this.$set(this, 'userLoading', true)
this.$axios.post(loc() + '/getSignUser', {
groupId: this.currentGroupId,
groupLeaderUserId: groupLeaderUserId,
groupLeaderLoginName: groupLeaderLoginName,
noGroupLeader: noGroupLeader,
pageNumber: 1,
pageSize: 1000
}).then((res) => {
if (res.code === 0) {
this.$set(this, 'signUserData', res.data.list || [])
this.$set(this, 'userVisible', true)
}
}).finally(() => {
this.$set(this, 'userLoading', false)
})
},
deleteUser(row) {
this.$confirm('您确定要删除该人员吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post(loc() + '/deleteJoinUser', {id: row.id})
.then((res) => {
if (res.code === 0) {
this.$message.success('删除成功')
this.$set(this, 'signUserData', this.signUserData.filter((item) => item.id !== row.id))
this.pageData()
} else {
this.$message.error(res.msg)
}
})
}).catch(() => {})
}
},
created() {
this.getTravelAgency()
this.getFlexibleGroupList()
this.pageData()
}
})
</script>
<!--# } #-->
@@ -1,26 +1,431 @@
<!--# layout("/layouts/platform.html"){ #-->
<style>
.line-statistics-query-row {
min-height: 70px;
display: flex;
align-items: center;
box-sizing: border-box;
}
.line-statistics-query-row:not(:last-child) {
border-bottom: 1px dashed #e6e6e6;
}
.line-statistics-query-content {
width: 100%;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never"><search @search="doSearch">
<search-item label="年度范围"><el-date-picker v-model="yearRange" type="yearrange" value-format="yyyy" range-separator="至" @change="yearChange"></el-date-picker></search-item>
<search-item label="所属工会"><el-select v-model="pageForm.unionId" filterable clearable @change="doSearch"><el-option v-for="item in unionOptions" :key="item.id" :label="item.name" :value="item.id"></el-option></el-select></search-item>
<search-item label="线路类型"><el-select v-model="pageForm.regionalNature" clearable @change="filterChange"><el-option label="省内" value="省内"></el-option><el-option label="省外" value="省外"></el-option></el-select></search-item>
<search-item label="组织方式"><el-select v-model="pageForm.signUpMode" clearable @change="filterChange"><el-option label="分工会组织" value="1"></el-option><el-option label="校工会组织" value="2"></el-option></el-select></search-item>
<search-item label="线路"><el-select v-model="pageForm.takePartInLineId" filterable clearable @change="doSearch"><el-option v-for="item in lineOptions" :key="item.id" :label="item.lineName+'-'+item.regionalNature+'【'+(item.lotName||'')+'】('+item.signUpMode+''" :value="item.id"></el-option></el-select></search-item>
<search-item label="标段"><el-select v-model="pageForm.lotId" filterable clearable @change="doSearch"><el-option v-for="item in lots" :key="item.id" :label="item.lotName" :value="item.id"></el-option></el-select></search-item>
</search></el-card>
<el-card shadow="never" class="mt20"><table-tool label="线路报名统计"></table-tool>
<el-table v-loading="tableLoading" :data="tableData" :size="tableSize"><el-table-column type="index" :index="indexMethod" label="序号" width="60"></el-table-column><el-table-column prop="year" label="年度" width="70"></el-table-column><el-table-column prop="lineName" label="线路名称" min-width="180"></el-table-column><el-table-column prop="linePlayTime" label="出行时间" width="150"></el-table-column><el-table-column prop="travelAgencyName" label="承担旅行社" min-width="150"></el-table-column><el-table-column prop="unionName" label="选择线路工会"></el-table-column><el-table-column prop="regionalNature" label="线路类型" width="90"></el-table-column><el-table-column prop="minimumGroupSize" label="最少成团人数" width="110"></el-table-column><el-table-column label="报名人数(家属)" width="130"><template v-slot="{row}"><el-link type="primary" @click="openUsers(row)">{{Number(row.lineNum||0)+Number(row.signUpUserFamilyNum||0)}}{{row.signUpUserFamilyNum||0}}</el-link></template></el-table-column><el-table-column label="操作" width="300"><template v-slot="{row}"><el-button size="mini" @click="openUsers(row)">查看人员</el-button><el-button type="success" size="mini" @click="notice(row,true,true)">成团通知</el-button><el-dropdown class="ml10"><el-button type="warning" size="mini">未成团通知<i class="el-icon-arrow-down el-icon--right"></i></el-button><el-dropdown-menu slot="dropdown"><el-dropdown-item @click.native="notice(row,false,true)">保留报名记录</el-dropdown-item><el-dropdown-item @click.native="notice(row,false,false)">删除报名记录</el-dropdown-item></el-dropdown-menu></el-dropdown></template></el-table-column></el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
<el-dialog title="线路报名人员" :visible.sync="userVisible" width="1050px" append-to-body><div class="mb10"><el-input v-model="userPage.searchKeyword" placeholder="姓名或工号" clearable style="width:220px" @keyup.enter.native="loadUsers"></el-input><el-button type="primary" class="ml10" @click="loadUsers">查询</el-button></div><el-table v-loading="userLoading" :data="users"><el-table-column type="index" label="序号" width="60"></el-table-column><el-table-column prop="loginName" label="工号"></el-table-column><el-table-column prop="userName" label="姓名"></el-table-column><el-table-column prop="unionName" label="所属工会"></el-table-column><el-table-column prop="unitName" label="所属单位"></el-table-column><el-table-column prop="familyCount" label="家属人数" width="90"></el-table-column><el-table-column prop="linePlayTime" label="出行时间"></el-table-column></el-table><el-pagination class="mt20" background layout="total, sizes, prev, pager, next" :current-page="userPage.pageNumber" :page-size="userPage.pageSize" :total="userPage.totalCount" @current-change="userPageChange" @size-change="userSizeChange"></el-pagination></el-dialog>
<guava ref="guava">
<template>
<el-card shadow="never">
<el-row class="line-statistics-query-row" type="flex" align="middle">
<el-col class="line-statistics-query-content">
<el-row :gutter="30">
<el-col :span="12">
<span>&emsp;&emsp;度:</span>
<el-date-picker v-model="pageForm.startYear" type="year" value-format="yyyy"
:clearable="false" placeholder="选择年" style="width: 38%"
@change="yearChange"></el-date-picker>
<span></span>
<el-date-picker v-model="pageForm.endYear" type="year" value-format="yyyy"
:clearable="false" placeholder="选择年" style="width: 38%"
@change="yearChange"></el-date-picker>
</el-col>
<el-col :span="12">
<span>所属工会:</span>
<el-select v-model="pageForm.unionId" filterable clearable placeholder="请选择所属工会"
style="width: 80%" @change="doSearch">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name"
:value="item.id"></el-option>
</el-select>
</el-col>
</el-row>
</el-col>
</el-row>
<el-row class="line-statistics-query-row" type="flex" align="middle">
<el-col class="line-statistics-query-content">
<el-row :gutter="30">
<el-col :span="12">
<span>线路类型:</span>
<el-select v-model="pageForm.regionalNature" filterable placeholder="请选择线路类型"
style="width: 80%" @change="lineTypeChange">
<el-option label="全部" value=""></el-option>
<el-option label="省内" value="省内"></el-option>
<el-option label="省外" value="省外"></el-option>
</el-select>
</el-col>
<el-col :span="12">
<span>线&emsp;&emsp;路:</span>
<el-select v-model="pageForm.takePartInLineId" filterable clearable
placeholder="请选择线路" style="width: 80%" @change="lineChange">
<el-option v-for="item in lineOptions" :key="item.id"
:label="item.lineName+'-'+item.regionalNature+'【'+(item.lotName||'')+'】('+item.signUpMode+''"
:value="item.id"></el-option>
</el-select>
</el-col>
</el-row>
</el-col>
</el-row>
<el-row class="line-statistics-query-row" type="flex" align="middle">
<el-col class="line-statistics-query-content">
<el-row :gutter="30">
<el-col :span="12">
<span>&emsp;&emsp;段:</span>
<el-select v-model="pageForm.lotId" filterable clearable placeholder="请选择标段"
style="width: 80%" @change="doSearch">
<el-option v-for="item in lots" :key="item.id" :label="item.lotName"
:value="item.id"></el-option>
</el-select>
</el-col>
<el-col :span="12">
<span>出行时间:</span>
<el-select v-model="pageForm.selectId" filterable clearable placeholder="请选择出行时间"
style="width: 80%" @change="doSearch">
<el-option v-for="item in linePlayTimes" :key="item.selectId" :label="item.times"
:value="item.selectId"></el-option>
</el-select>
</el-col>
</el-row>
</el-col>
</el-row>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool label="数据列表">
<el-button icon="el-icon-s-promotion" size="small" type="primary" class="mr10" @click="doExport">
导出成团线路人员
</el-button>
</table-tool>
<el-table ref="table" row-key="lineUId" :data="tableData" v-loading="tableLoading" style="width: 100%"
@sort-change="pageOrder">
<el-table-column align="center" header-align="center" type="index" label="序号" :index="indexMethod"
width="80"></el-table-column>
<el-table-column v-for="column in tableColumns" :key="column.prop" align="center"
header-align="center" show-overflow-tooltip :label="column.label"
:prop="column.prop" :width="column.width" :sortable="column.sortable">
<template v-slot="{row}">
<el-link v-if="column.prop==='lineName'" type="primary" @click="openLine(row)">
{{row.lineName}}
</el-link>
<el-link v-else-if="column.prop==='lineNum'" type="primary" @click="openUserData(row)">
{{Number(row.lineNum||0)+Number(row.signUpUserFamilyNum||0)}}{{row.signUpUserFamilyNum||0}}
</el-link>
<span v-else>{{row[column.prop]}}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="380">
<template v-slot="{row}">
<el-button size="mini" type="primary" @click="openUserData(row)">查看人员</el-button>
<el-button size="mini" type="primary" @click="sendNotice(row,true,true)">发送成团通知
</el-button>
<el-dropdown class="ml10 mr10" trigger="click">
<el-button size="mini" type="primary">发送未成团通知<i
class="el-icon-arrow-down el-icon--right"></i></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="sendNotice(row,false,true)">保留报名记录
</el-dropdown-item>
<el-dropdown-item @click.native="sendNotice(row,false,false)">删除报名记录
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
<template #public>
<line-info ref="viewLineInfo"></line-info>
</template>
<template #edit>
<el-card shadow="never">
<div class="search">
<div class="search-item">
<div class="search-item-label">工号姓名</div>
<div class="search-item-option">
<el-input v-model="userPage.searchKeyword" maxlength="30" clearable></el-input>
</div>
</div>
<div class="search-item" v-if="$auth.hasRole('SYSADMIN') || $auth.hasRole('SCHOOL_UNION_ADMIN')">
<div class="search-item-label">所属工会</div>
<div class="search-item-option">
<el-select v-model="userPage.unionId" filterable clearable style="width: 100%"
@change="flushUnits">
<el-option v-for="item in unionOptions" :key="item.id" :label="item.name"
:value="item.id"></el-option>
</el-select>
</div>
</div>
<div class="search-item">
<div class="search-item-label">所属单位</div>
<div class="search-item-option">
<el-select v-model="userPage.unitId" filterable clearable style="width: 100%">
<el-option v-for="item in units" :key="item.id" :label="item.name"
:value="item.id"></el-option>
</el-select>
</div>
</div>
<div class="search-query">
<el-button type="primary" icon="el-icon-search" @click="loadUsers">搜索</el-button>
</div>
</div>
</el-card>
<el-card shadow="never" class="mt10">
<el-table :data="users" v-loading="userLoading">
<el-table-column v-for="column in userColumns" :key="column.prop" align="center"
header-align="center" show-overflow-tooltip :label="column.label"
:prop="column.prop" :sortable="column.sortable">
<template v-slot="{row}">
<el-link v-if="column.prop==='familyCount'" type="primary">
{{Number(row.familyCount||0)>0?'携带':'未携带'}}{{row.familyCount||0}}
</el-link>
<span v-else>{{row[column.prop]}}</span></template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container" style="margin-bottom: 0">
<el-pagination :current-page="userPage.pageNumber" :page-size="userPage.pageSize"
:page-sizes="[5,10,20,30,50]" :total="userPage.totalCount"
layout="total, sizes, prev, pager, next" @size-change="userSizeChange"
@current-change="userPageChange"></el-pagination>
</el-row>
</el-card>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
new Vue({el:'#app',mixins:[initTableMixins],data(){const year=new Date().getFullYear().toString();return{yearRange:[year,year],pageForm:{pageNumber:1,pageSize:10,totalCount:0,startYear:year,endYear:year,unionId:'',regionalNature:'',signUpMode:'',takePartInLineId:'',lotId:'',selectId:''},unionOptions:[],lineOptions:[],lots:[],userVisible:false,userLoading:false,currentRow:{},users:[],userPage:{pageNumber:1,pageSize:10,totalCount:0,searchKeyword:'',unionId:'',unitId:''}}},methods:{
pageData(){this.$set(this,'tableLoading',true);this.$axios.post(loc()+'/pageData',this.pageForm).then((res)=>{if(res.code===0){this.$set(this,'tableData',res.data.list||[]);this.$set(this.pageForm,'totalCount',res.data.totalCount||0)}}).finally(()=>{this.$set(this,'tableLoading',false)})},
yearChange(value){this.$set(this.pageForm,'startYear',value[0]);this.$set(this.pageForm,'endYear',value[1]);this.filterChange()},filterChange(){this.$set(this.pageForm,'takePartInLineId','');this.loadLines();this.doSearch()},
loadLines(){this.$axios.post(loc()+'/getLineOptions',{startYear:this.pageForm.startYear,endYear:this.pageForm.endYear,signUpMode:this.pageForm.signUpMode,regionalNature:this.pageForm.regionalNature}).then((res)=>{if(res.code===0)this.$set(this,'lineOptions',res.data||[])})},
openUsers(row){this.$set(this,'currentRow',row);this.$set(this.userPage,'pageNumber',1);this.$set(this,'userVisible',true);this.loadUsers()},loadUsers(){this.$set(this,'userLoading',true);const params=Object.assign({},this.userPage,{takePartLineId:this.currentRow.lineUId});this.$axios.post(loc()+'/getUserDateByLine',params).then((res)=>{if(res.code===0){this.$set(this,'users',res.data.list||[]);this.$set(this.userPage,'totalCount',res.data.totalCount||0)}}).finally(()=>{this.$set(this,'userLoading',false)})},userPageChange(value){this.$set(this.userPage,'pageNumber',value);this.loadUsers()},userSizeChange(value){this.$set(this.userPage,'pageSize',value);this.$set(this.userPage,'pageNumber',1);this.loadUsers()},
notice(row,success,keep){const countUrl=success?'/countSuccessNotice':'/countFailNotice';this.$axios.post(loc()+countUrl,{id:row.lineUId}).then((res)=>{if(res.code!==0){this.$message.warning(res.msg);return}const data=res.data||{};if(!data.sendCount){this.$message.warning(data.msg);return}this.$prompt('当前将通过'+data.sendTypeName+'发送'+data.sendCount+'人,请确认消息内容。','提示',{inputType:'textarea',inputValue:data.content||'',inputValidator:(value)=>!!(value&&value.trim()),inputErrorMessage:'消息内容不能为空'}).then((prompt)=>{const url=success?'/sendSuccess':'/sendFail';this.$axios.post(loc()+url,{id:row.lineUId,type:keep,content:prompt.value}).then((sendRes)=>{if(sendRes.code===0){const result=sendRes.data||{};this.$message.success(result.msg||'通知发送完成');this.pageData()}else this.$message.warning(sendRes.msg)})}).catch(()=>{})})}
},created(){this.$businessTool.listUnion().then((res)=>{this.$set(this,'unionOptions',res||[])});this.$axios.post('/platform/recuperation/config/fetchOne').then((res)=>{if(res.code===0)this.$set(this,'lots',(res.data&&res.data.lots)||[])});this.loadLines();this.pageData()}})
<!--#include('../line/info.js'){}#-->
new Vue({
el: '#app',
mixins: [initTableMixins],
components: {'line-info': info},
data() {
const year = new Date().getFullYear().toString()
return {
pageForm: {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
startYear: year,
endYear: year,
unionId: '',
regionalNature: '',
takePartInLineId: '',
lotId: '',
selectId: '',
signUpMode: ''
},
tableColumns: [
{prop: 'year', label: '年度', width: 60}, {
prop: 'lineName',
label: '线路名称',
sortable: true,
width: 200
},
{prop: 'linePlayTime', label: '出行时间', width: 180}, {
prop: 'travelAgencyName',
label: '承担旅行社',
sortable: true,
width: 180
},
{prop: 'unionName', label: '选择线路工会', sortable: true}, {
prop: 'regionalNature',
label: '线路类型',
sortable: true
},
{prop: 'contact', label: '联系人', sortable: true, checked: 0}, {
prop: 'contactMobileNumber',
label: '联系方式',
checked: 0
},
{prop: 'minimumGroupSize', label: '最少成团人数(含家属)', width: 140}, {
prop: 'lineNum',
label: '报名人数(含家属)',
width: 140
}
],
unionOptions: [],
lineOptions: [],
linePlayTimes: [],
lots: [],
units: [],
lineUId: '',
users: [],
userLoading: false,
userPage: {pageNumber: 1, pageSize: 10, totalCount: 0, searchKeyword: '', unionId: '', unitId: ''},
userColumns: [
{prop: 'loginName', label: '工号'}, {prop: 'userName', label: '姓名'}, {
prop: 'unionName',
label: '所属工会'
},
{prop: 'unitName', label: '所属单位', sortable: true}, {prop: 'familyCount', label: '是否携带家属'},
{prop: 'linePlayTime', label: '出行时间', sortable: true}
]
}
},
methods: {
pageData() {
this.$set(this, 'tableLoading', true)
this.$axios.post(loc() + '/pageData', this.pageForm).then((res) => {
if (res.code === 0) {
this.$set(this, 'tableData', res.data.list || []);
this.$set(this.pageForm, 'totalCount', res.data.totalCount || 0)
}
}).finally(() => {
this.$set(this, 'tableLoading', false)
})
},
yearChange() {
this.$set(this.pageForm, 'takePartInLineId', '');
this.$set(this.pageForm, 'selectId', '');
this.$set(this, 'linePlayTimes', [])
this.loadLines();
this.doSearch()
},
lineTypeChange() {
this.$set(this.pageForm, 'takePartInLineId', '');
this.$set(this.pageForm, 'selectId', '');
this.$set(this, 'linePlayTimes', [])
this.loadLines();
this.doSearch()
},
lineChange() {
this.$set(this.pageForm, 'selectId', '');
this.loadLinePlayTimes();
this.doSearch()
},
loadLines() {
this.$axios.post(loc() + '/getLineOptions', {
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear,
signUpMode: '',
regionalNature: this.pageForm.regionalNature
})
.then((res) => {
if (res.code === 0) this.$set(this, 'lineOptions', res.data || [])
})
},
loadLinePlayTimes() {
if (!this.pageForm.takePartInLineId) {
this.$set(this, 'linePlayTimes', []);
return
}
this.$axios.post(loc() + '/getLinePlayTimes', {
lineId: this.pageForm.takePartInLineId,
startYear: this.pageForm.startYear,
endYear: this.pageForm.endYear
})
.then((res) => {
if (res.code === 0) this.$set(this, 'linePlayTimes', res.data || [])
})
},
doExport() {
this.$downLoad(loc() + '/doExport', this.pageForm)
},
openLine(row) {
this.$refs.guava.public()
this.$nextTick(() => {
this.$refs.viewLineInfo.findOne(row.lineId, row.usUnionId)
this.$refs.viewLineInfo.findUnionSelectLineData(row.lineId, row.usUnionId)
this.$set(this.$refs.viewLineInfo, 'showTravelTimePeriod', true)
})
},
openUserData(row) {
this.$set(this, 'lineUId', row.lineUId)
this.$set(this, 'userPage', {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
searchKeyword: '',
unionId: '',
unitId: ''
})
this.$set(this, 'units', []);
this.$refs.guava.edit();
this.loadUsers()
},
flushUnits() {
this.$set(this.userPage, 'unitId', '')
if (!this.userPage.unionId) {
this.$set(this, 'units', []);
return
}
getUnits(this.userPage.unionId).then((res) => {
this.$set(this, 'units', res || [])
})
},
loadUsers() {
this.$set(this, 'userLoading', true)
const params = Object.assign({}, this.userPage, {takePartLineId: this.lineUId})
this.$axios.post(loc() + '/getUserDateByLine', params).then((res) => {
if (res.code === 0) {
this.$set(this, 'users', res.data.list || []);
this.$set(this.userPage, 'totalCount', res.data.totalCount || 0)
}
}).finally(() => {
this.$set(this, 'userLoading', false)
})
},
userPageChange(value) {
this.$set(this.userPage, 'pageNumber', value);
this.loadUsers()
},
userSizeChange(value) {
this.$set(this.userPage, 'pageSize', value);
this.$set(this.userPage, 'pageNumber', 1);
this.loadUsers()
},
sendNotice(row, success, keep) {
const countUrl = success ? '/countSuccessNotice' : '/countFailNotice'
this.$axios.post(loc() + countUrl, {id: row.lineUId}).then((res) => {
if (res.code !== 0) {
this.$message.warning(res.msg);
return
}
const data = res.data || {}
if (!data.sendCount) {
this.$message.warning(data.msg || '当前暂无需要发送通知的报名人员');
return
}
const noticeName = success ? '成团通知' : '未成团通知'
const recordAction = success ? '' : ',并' + (keep ? '保留' : '删除') + '报名记录'
const message = '当前将通过' + (data.sendTypeName || '微信、钉钉') + '发送' + noticeName + data.sendCount + '人' + recordAction + ',请确认或修改消息内容。'
this.$prompt(message, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputType: 'textarea',
inputValue: data.content || '',
inputValidator: (value) => !!(value && value.trim()),
inputErrorMessage: '消息内容不能为空'
}).then((prompt) => {
const url = success ? '/sendSuccess' : '/sendFail'
const params = {id: row.lineUId, content: prompt.value.trim()}
if (!success) this.$set(params, 'type', keep)
this.$axios.post(loc() + url, params).then((sendRes) => {
if (sendRes.code === 0) {
const result = sendRes.data || {};
this.$message.success(result.msg || '通知发送完成');
this.pageData()
} else this.$message.warning(sendRes.msg)
})
}).catch(() => {
})
})
}
},
created() {
this.$businessTool.listUnion().then((res) => {
this.$set(this, 'unionOptions', res || [])
})
this.$axios.post('/platform/recuperation/config/fetchOne').then((res) => {
if (res.code === 0) {
const source = (res.data && res.data.lots) || []
const lots = source.filter((item, index, list) => item && item.id && list.findIndex((lot) => lot && lot.id === item.id) === index)
this.$set(this, 'lots', lots)
}
})
this.loadLines();
this.pageData()
}
})
</script>
<!--# } #-->
@@ -1,19 +1,688 @@
<!--# layout("/layouts/platform.html"){ #-->
<!--#
layout("/layouts/platform.html"){
#-->
<style>
.center-col {
background-color: white;
position: relative;
box-sizing: border-box;
padding: 20px;
width: calc((100% - (20px * 2)) / 2);
}
.col-title {
color: rgba(0, 0, 0, .45);
font-size: 14px;
margin-bottom: 10px;
}
.col-button {
margin-bottom: 30px;
}
.rating-summary {
float: right;
color: #ff9900;
font-size: 14px;
}
.travel-agency-summary {
display: flex;
padding: 22px 0;
}
.travel-agency-summary-item {
display: flex;
flex: 1;
align-items: center;
min-width: 0;
padding: 0 24px;
border-right: 1px solid #f0f0f0;
}
.travel-agency-summary-item:last-child {
border-right: none;
}
.travel-agency-summary-icon {
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 48px;
height: 48px;
margin-right: 14px;
border-radius: 50%;
color: #1867b0;
background-color: #eef6ff;
font-size: 24px;
}
.travel-agency-summary-label {
color: rgba(0, 0, 0, .65);
font-size: 14px;
}
.travel-agency-summary-score {
margin-top: 2px;
color: #1867b0;
font-size: 26px;
font-weight: 600;
line-height: 30px;
}
.travel-agency-summary-score span {
margin-left: 2px;
font-size: 14px;
font-weight: normal;
}
.travel-agency-summary-count {
margin-top: 3px;
color: rgba(0, 0, 0, .45);
font-size: 13px;
}
.scheme-service-card {
min-height: 360px;
}
.scheme-service-title {
margin-bottom: 14px;
color: rgba(0, 0, 0, .85);
font-size: 16px;
font-weight: 500;
}
.scheme-type-switch {
margin-bottom: 18px;
}
.scheme-selector {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 18px;
}
.scheme-selector-item {
display: flex;
align-items: center;
justify-content: space-between;
width: calc((100% - 36px) / 4);
min-width: 200px;
height: 56px;
margin: 0 !important;
padding: 0 16px;
text-align: left;
}
.scheme-selector-name {
overflow: hidden;
margin-right: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.scheme-selector-score {
flex: 0 0 auto;
font-size: 15px;
}
.scheme-detail {
padding: 22px 24px;
border: 1px solid #e6eef8;
border-radius: 4px;
background-color: #fcfdff;
}
.scheme-detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 22px;
}
.scheme-detail-name {
color: rgba(0, 0, 0, .85);
font-size: 20px;
font-weight: 500;
}
.scheme-detail-agency {
margin-top: 8px;
color: rgba(0, 0, 0, .45);
font-size: 14px;
}
.scheme-detail-composite {
min-width: 130px;
text-align: right;
}
.scheme-detail-composite-label {
color: rgba(0, 0, 0, .45);
font-size: 13px;
}
.scheme-detail-composite-score {
margin-top: 3px;
color: #1867b0;
font-size: 30px;
font-weight: 600;
}
.scheme-detail-composite-score span {
margin-left: 2px;
font-size: 14px;
font-weight: normal;
}
.scheme-detail-composite-count {
margin-top: 3px;
color: rgba(0, 0, 0, .45);
font-size: 13px;
}
.scheme-metric-list {
display: flex;
border-top: 1px solid #edf1f5;
}
.scheme-metric-item {
flex: 1;
min-width: 0;
padding: 18px 14px 8px;
text-align: center;
border-right: 1px solid #edf1f5;
}
.scheme-metric-item:last-child {
border-right: none;
}
.scheme-metric-icon {
color: #1867b0;
font-size: 22px;
}
.scheme-metric-label {
margin-top: 8px;
color: rgba(0, 0, 0, .65);
font-size: 14px;
}
.scheme-metric-score {
margin-top: 5px;
color: #1867b0;
font-size: 23px;
font-weight: 600;
}
.scheme-metric-score span {
margin-left: 2px;
font-size: 13px;
font-weight: normal;
}
.scheme-metric-count {
margin-top: 3px;
color: rgba(0, 0, 0, .45);
font-size: 12px;
}
.scheme-feedback-action {
padding-top: 18px;
text-align: center;
border-top: 1px solid #edf1f5;
}
.scheme-feedback-list {
max-height: 500px;
overflow-y: auto;
}
.scheme-feedback-row {
padding: 16px 4px;
border-bottom: 1px solid #edf1f5;
}
.scheme-feedback-row:first-child {
padding-top: 0;
}
.scheme-feedback-row:last-child {
border-bottom: none;
}
.scheme-feedback-user {
color: rgba(0, 0, 0, .85);
font-size: 15px;
font-weight: 500;
}
.scheme-feedback-union {
margin-left: 10px;
color: rgba(0, 0, 0, .45);
font-size: 13px;
font-weight: normal;
}
.scheme-feedback-content {
margin-top: 9px;
color: rgba(0, 0, 0, .65);
line-height: 22px;
white-space: pre-wrap;
}
.scheme-empty {
padding: 64px 0;
color: rgba(0, 0, 0, .45);
text-align: center;
}
.feedback-content {
line-height: 22px;
white-space: pre-wrap;
}
.feedback-summary {
display: inline-block;
max-width: 100%;
overflow: hidden;
vertical-align: middle;
text-overflow: ellipsis;
white-space: nowrap;
}
.feedback-score-grid {
display: flex;
flex-wrap: wrap;
margin: 0 -6px 14px;
}
.feedback-score-item {
box-sizing: border-box;
width: 33.33%;
padding: 6px;
}
.feedback-score-item-inner {
padding: 10px 12px;
border-radius: 4px;
background-color: #f7faff;
}
.feedback-score-label {
color: rgba(0, 0, 0, .45);
font-size: 13px;
}
.feedback-score-value {
margin-top: 3px;
color: #1867b0;
font-size: 18px;
font-weight: 500;
}
.feedback-pagination {
margin-top: 20px;
text-align: right;
}
</style>
<div id="app" v-cloak>
<el-card shadow="never"><el-row type="flex" align="middle"><el-col :span="8"><span>统计年度:</span><el-date-picker v-model="pageForm.year" type="year" value-format="yyyy" :clearable="false" @change="yearChange"></el-date-picker></el-col><el-col :span="16"><el-tabs v-model="activeTab" @tab-click="tabChange"><el-tab-pane label="满意度统计" name="summary"></el-tab-pane><el-tab-pane label="评价建议明细" name="feedback"></el-tab-pane><el-tab-pane label="评价人员名单" name="users"></el-tab-pane></el-tabs></el-col></el-row></el-card>
<template v-if="activeTab==='summary'">
<el-row :gutter="20" class="mt20"><el-col :span="8"><el-card shadow="never"><div slot="header">旅行社综合满意度</div><el-table v-loading="summaryLoading" :data="agencyRatings" height="530"><el-table-column type="index" label="排名" width="65"></el-table-column><el-table-column prop="travelAgencyName" label="旅行社" show-overflow-tooltip></el-table-column><el-table-column prop="avg" label="综合评分" width="90"><template v-slot="{row}">{{score(row.avg)}}</template></el-table-column><el-table-column prop="ratingCount" label="评价人数" width="85"></el-table-column></el-table></el-card></el-col><el-col :span="16"><el-card shadow="never"><div slot="header">方案服务评分</div><el-table v-loading="summaryLoading" :data="schemeRatings" height="530"><el-table-column prop="typeName" label="类型" width="85"></el-table-column><el-table-column prop="schemeName" label="方案名称" min-width="150" show-overflow-tooltip></el-table-column><el-table-column prop="travelAgencyName" label="旅行社" min-width="130" show-overflow-tooltip></el-table-column><el-table-column label="旅行社" width="75"><template v-slot="{row}">{{score(row.travelAgencyAvg)}}</template></el-table-column><el-table-column label="行程" width="65"><template v-slot="{row}">{{score(row.journeyAvg)}}</template></el-table-column><el-table-column label="住宿/酒店" width="90"><template v-slot="{row}">{{score(row.accommodationAvg)}}</template></el-table-column><el-table-column label="餐饮" width="65"><template v-slot="{row}">{{score(row.diningAvg)}}</template></el-table-column><el-table-column label="交通" width="65"><template v-slot="{row}">{{score(row.transportationAvg)}}</template></el-table-column><el-table-column label="综合" width="65"><template v-slot="{row}"><strong>{{score(row.compositeAvg)}}</strong></template></el-table-column><el-table-column prop="ratingCount" label="人数" width="65"></el-table-column></el-table></el-card></el-col></el-row>
</template>
<template v-if="activeTab==='feedback'"><el-card shadow="never" class="mt20"><table-tool label="评价建议"><el-select v-model="feedbackType" size="small" style="width:140px" @change="loadFeedback"><el-option v-for="item in ['全部','线路','灵活组团','定点']" :key="item" :label="item" :value="item"></el-option></el-select><el-button type="primary" size="small" class="ml10" @click="exportFeedback">导出评价明细</el-button></table-tool><el-table v-loading="feedbackLoading" :data="feedbackDetails"><el-table-column type="index" label="序号" width="60"></el-table-column><el-table-column prop="typeName" label="报名方式" width="100"></el-table-column><el-table-column prop="schemeName" label="方案名称" min-width="160"></el-table-column><el-table-column prop="travelAgencyName" label="旅行社" min-width="140"></el-table-column><el-table-column prop="userName" label="评价人" width="100"></el-table-column><el-table-column prop="unitName" label="所属单位" min-width="140"></el-table-column><el-table-column prop="compositeScore" label="综合评分" width="100"></el-table-column><el-table-column prop="feedbackContent" label="评价建议" min-width="240" show-overflow-tooltip></el-table-column><el-table-column label="操作" width="80"><template v-slot="{row}"><el-button type="primary" size="mini" @click="viewFeedback(row)">查看</el-button></template></el-table-column></el-table></el-card></template>
<template v-if="activeTab==='users'"><el-card shadow="never" class="mt20"><search @search="doSearch"><search-item label="线路"><el-select v-model="pageForm.lineId" filterable clearable @change="doSearch"><el-option v-for="item in lineList" :key="item.id" :label="item.lineName+''+item.unionName+''" :value="item.id"></el-option></el-select></search-item><search-item label="姓名/工号"><el-input v-model="pageForm.searchKeyword" clearable @keyup.enter.native="doSearch"></el-input></search-item><search-item label="所属工会"><el-select v-model="pageForm.unionId" filterable clearable @change="doSearch"><el-option v-for="item in unionList" :key="item.id" :label="item.name" :value="item.id"></el-option></el-select></search-item><search-item label="所属单位"><el-select v-model="pageForm.unitId" filterable clearable @change="doSearch"><el-option v-for="item in unitList" :key="item.id" :label="item.name" :value="item.id"></el-option></el-select></search-item><search-item label="评分"><el-select v-model="pageForm.evaluateScore" clearable @change="doSearch"><el-option v-for="item in ['满意','一般','不满意']" :key="item" :label="item" :value="item"></el-option></el-select></search-item></search><table-tool label="评价人员名单"><el-button type="primary" size="small" @click="exportEvaluate">导出名单</el-button></table-tool><el-table v-loading="tableLoading" :data="tableData"><el-table-column type="index" :index="indexMethod" label="序号" width="60"></el-table-column><el-table-column prop="lineName" label="线路"></el-table-column><el-table-column prop="playStartTime" label="出行时间"></el-table-column><el-table-column prop="loginName" label="工号"></el-table-column><el-table-column prop="userName" label="姓名"></el-table-column><el-table-column prop="unitName" label="所属单位"></el-table-column><el-table-column prop="unionName" label="所属工会"></el-table-column><el-table-column prop="evaluateScore" label="评分"></el-table-column><el-table-column prop="evaluateText" label="评价" show-overflow-tooltip></el-table-column></el-table><!--#include("/layouts/pagination.html"){}#--></el-card></template>
<el-dialog title="评价详情" :visible.sync="feedbackVisible" width="650px" append-to-body><el-descriptions :column="2" border><el-descriptions-item label="报名方式">{{feedbackDetail.typeName}}</el-descriptions-item><el-descriptions-item label="综合评分">{{feedbackDetail.compositeScore}}</el-descriptions-item><el-descriptions-item label="旅行社评分">{{feedbackDetail.evaluationForTravelAgency}}</el-descriptions-item><el-descriptions-item label="住宿/酒店评分">{{feedbackDetail.evaluationForAccommodation}}</el-descriptions-item><el-descriptions-item label="行程评分">{{feedbackDetail.evaluationForJourney}}</el-descriptions-item><el-descriptions-item label="餐饮评分">{{feedbackDetail.evaluationForDining}}</el-descriptions-item><el-descriptions-item label="交通评分">{{feedbackDetail.evaluationForTransportation}}</el-descriptions-item><el-descriptions-item label="方案">{{feedbackDetail.schemeName}}</el-descriptions-item></el-descriptions><el-alert class="mt20" :closable="false" :title="feedbackDetail.feedbackContent||'暂无评价建议'" type="info"></el-alert></el-dialog>
<guava ref="guava">
<template>
<el-card shadow="never">
<div class="btn-group tool-button">
<el-date-picker
v-model="year"
type="year"
value-format="yyyy"
placeholder="选择年"
style="width: 100%" @change="doSearch">
</el-date-picker>
</div>
</el-card>
<el-card shadow="never" class="mt20">
<div class="travel-agency-summary">
<div class="travel-agency-summary-item" v-for="item in travelAgencyCompositeRatings" :key="item.travelAgencyName">
<div class="travel-agency-summary-icon">
<i class="el-icon-guide"></i>
</div>
<div>
<div class="travel-agency-summary-label">{{item.travelAgencyName}}</div>
<div class="travel-agency-summary-score" v-if="hasRatingScore(item.avg)">{{formatRatingScore(item.avg)}}<span></span></div>
<div class="travel-agency-summary-score" v-else>暂无评价</div>
<div class="travel-agency-summary-count">评价数 {{item.ratingCount}}</div>
</div>
</div>
</div>
</el-card>
<el-card shadow="never" class="mt20 scheme-service-card">
<div class="scheme-service-title">方案服务评分</div>
<el-radio-group class="scheme-type-switch" :value="schemeType" @input="changeSchemeType" size="medium">
<el-radio-button label="线路"></el-radio-button>
<el-radio-button label="灵活组团"></el-radio-button>
<el-radio-button label="定点"></el-radio-button>
</el-radio-group>
<div class="scheme-selector" v-if="getSchemeRatingsByType().length">
<el-button
v-for="item in getSchemeRatingsByType()"
:key="item.schemeId"
class="scheme-selector-item"
:type="isSelectedScheme(item) ? 'primary' : 'default'"
:plain="!isSelectedScheme(item)"
@click="selectScheme(item)">
<span class="scheme-selector-name">{{item.schemeName}}</span>
<span class="scheme-selector-score">{{formatRatingScore(item.compositeAvg)}} 分</span>
</el-button>
</div>
<div class="scheme-detail" v-if="getSelectedSchemeRating()">
<div class="scheme-detail-header">
<div>
<div class="scheme-detail-name">{{getSelectedSchemeRating().schemeName}}</div>
<div class="scheme-detail-agency">旅行社:{{getSelectedSchemeRating().travelAgencyName || '暂无旅行社信息'}}</div>
</div>
<div class="scheme-detail-composite">
<div class="scheme-detail-composite-label">综合评分</div>
<div class="scheme-detail-composite-score">{{formatRatingScore(getSelectedSchemeRating().compositeAvg)}}<span></span></div>
<div class="scheme-detail-composite-count">评价数 {{getSelectedSchemeRating().ratingCount || 0}}</div>
</div>
</div>
<div class="scheme-metric-list">
<div class="scheme-metric-item" v-for="item in getSelectedSchemeMetrics()" :key="item.key">
<i class="scheme-metric-icon" :class="item.icon"></i>
<div class="scheme-metric-label">{{item.label}}</div>
<div class="scheme-metric-score">{{formatRatingScore(item.avg)}}<span></span></div>
<div class="scheme-metric-count">评价数 {{item.count || 0}}</div>
</div>
</div>
<div class="scheme-feedback-action">
<el-button type="primary" plain @click="openSchemeFeedbackDialog">查看评价建议</el-button>
</div>
</div>
<div class="scheme-empty" v-else>暂无可查看的方案评分</div>
</el-card>
<el-card shadow="never" class="mt20">
<div class="col-title">
评价明细
<div style="float: right">
<el-button size="medium" type="primary" icon="el-icon-download" style="margin-right: 10px" @click="exportFeedbackDetails">导出</el-button>
<el-radio-group :value="feedbackType" @input="changeFeedbackType" size="medium">
<el-radio-button label="全部"></el-radio-button>
<el-radio-button label="线路"></el-radio-button>
<el-radio-button label="灵活组团"></el-radio-button>
<el-radio-button label="定点"></el-radio-button>
</el-radio-group>
</div>
</div>
<el-table :data="getPagedFeedbackDetails()" style="width: 100%">
<el-table-column align="center" header-align="center" type="index" label="序号" :index="feedbackIndexMethod" width="80"></el-table-column>
<el-table-column align="center" header-align="center" prop="typeName" label="报名方式" width="100"></el-table-column>
<el-table-column align="center" header-align="center" prop="projectName" label="方案名称" min-width="160" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" prop="travelAgencyName" label="旅行社" min-width="140" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" prop="userName" label="评价人" width="100"></el-table-column>
<el-table-column align="center" header-align="center" prop="unitName" label="所属单位" min-width="150" show-overflow-tooltip></el-table-column>
<el-table-column align="center" header-align="center" label="综合评分" width="110">
<template slot-scope="scope">
{{formatFeedbackScore(scope.row.compositeScore)}}
</template>
</el-table-column>
<el-table-column align="left" header-align="center" label="评价建议" min-width="250">
<template slot-scope="scope">
<span class="feedback-summary">{{getFeedbackContentSummary(scope.row.feedbackContent)}}</span>
</template>
</el-table-column>
<el-table-column align="center" header-align="center" label="操作" width="90">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="openFeedbackDetail(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="feedback-pagination"
:current-page="feedbackPage"
:page-size="feedbackPageSize"
:page-sizes="[10, 20, 50, 100]"
:total="getFilteredFeedbackDetails().length"
layout="total, sizes, prev, pager, next, jumper"
@current-change="changeFeedbackPage"
@size-change="changeFeedbackPageSize">
</el-pagination>
</el-card>
</template>
</guava>
<el-dialog title="评价详情" :visible="feedbackDetailVisible" width="620px" @close="closeFeedbackDetail">
<div class="feedback-score-grid">
<div class="feedback-score-item">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">综合评分</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.compositeScore)}}</div>
</div>
</div>
<div class="feedback-score-item">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">旅行社评分</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.evaluationForTravelAgency)}}</div>
</div>
</div>
<div class="feedback-score-item" v-if="feedbackDetail.typeName === '线路'">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">行程评分</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.evaluationForJourney)}}</div>
</div>
</div>
<div class="feedback-score-item">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">{{feedbackDetail.typeName === '线路' ? '住宿评分' : '酒店评分'}}</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.evaluationForAccommodation)}}</div>
</div>
</div>
<div class="feedback-score-item" v-if="feedbackDetail.typeName === '线路'">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">餐饮评分</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.evaluationForDining)}}</div>
</div>
</div>
<div class="feedback-score-item" v-if="feedbackDetail.typeName === '线路'">
<div class="feedback-score-item-inner">
<div class="feedback-score-label">交通评分</div>
<div class="feedback-score-value">{{formatFeedbackScore(feedbackDetail.evaluationForTransportation)}}</div>
</div>
</div>
</div>
<div class="feedback-content">{{feedbackDetail.feedbackContent || '暂无评价建议'}}</div>
</el-dialog>
<el-dialog title="评价建议" :visible="schemeFeedbackVisible" width="720px" @close="closeSchemeFeedbackDialog">
<div class="scheme-feedback-list" v-if="getSelectedSchemeFeedbackDetails().length">
<div class="scheme-feedback-row" v-for="item in getSelectedSchemeFeedbackDetails()" :key="item.id">
<div class="scheme-feedback-user">
{{item.userName || '匿名评价人'}}
<span class="scheme-feedback-union">{{item.unitName || '暂无所属单位'}}</span>
</div>
<div class="scheme-feedback-content">{{item.feedbackContent}}</div>
</div>
</div>
<div class="scheme-empty" v-else>当前方案暂无评价建议</div>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({el:'#app',mixins:[initTableMixins],data(){return{activeTab:'summary',pageForm:{pageNumber:1,pageSize:10,totalCount:0,year:new Date().getFullYear().toString(),lineId:'',searchKeyword:'',unionId:'',unitId:'',evaluateScore:''},unionList:[],unitList:[],lineList:[],summaryLoading:false,feedbackLoading:false,agencyRatings:[],schemeRatings:[],feedbackDetails:[],feedbackType:'全部',feedbackVisible:false,feedbackDetail:{}}},methods:{
score(value){return value===null||value===undefined||value===''?'—':Number(value).toFixed(1)},yearChange(){this.$set(this.pageForm,'lineId','');this.loadLines();this.loadSummary();if(this.activeTab==='feedback')this.loadFeedback();if(this.activeTab==='users')this.doSearch()},tabChange(){if(this.activeTab==='summary')this.loadSummary();if(this.activeTab==='feedback')this.loadFeedback();if(this.activeTab==='users')this.pageData()},
loadSummary(){this.$set(this,'summaryLoading',true);this.$axios.post(loc()+'/getSatisfactionRatingStatistics',{year:this.pageForm.year}).then((res)=>{if(res.code===0){this.$set(this,'agencyRatings',(res.data&&res.data.travelAgencyCompositeRatings)||[]);this.$set(this,'schemeRatings',(res.data&&res.data.schemeServiceRatings)||[])}}).finally(()=>{this.$set(this,'summaryLoading',false)})},
loadFeedback(){this.$set(this,'feedbackLoading',true);this.$axios.post(loc()+'/getSatisfactionFeedbackDetails',{year:this.pageForm.year,typeName:this.feedbackType}).then((res)=>{if(res.code===0)this.$set(this,'feedbackDetails',res.data||[])}).finally(()=>{this.$set(this,'feedbackLoading',false)})},exportFeedback(){this.$downLoad(loc()+'/exportSatisfactionFeedbackDetails',{year:this.pageForm.year,typeName:this.feedbackType})},viewFeedback(row){this.$set(this,'feedbackDetail',row);this.$set(this,'feedbackVisible',true)},
loadLines(){this.$axios.post(loc()+'/lineList',{year:this.pageForm.year}).then((res)=>{if(res.code===0)this.$set(this,'lineList',res.data||[])})},pageData(){this.$set(this,'tableLoading',true);this.$axios.post(loc()+'/pageData',this.pageForm).then((res)=>{if(res.code===0){this.$set(this,'tableData',res.data.list||[]);this.$set(this.pageForm,'totalCount',res.data.totalCount||0)}}).finally(()=>{this.$set(this,'tableLoading',false)})},exportEvaluate(){this.$downLoad(loc()+'/exportEvaluate',this.pageForm)}
},created(){this.$businessTool.listUnion().then((res)=>this.$set(this,'unionList',res||[]));this.$businessTool.listUnit().then((res)=>this.$set(this,'unitList',res||[]));this.loadLines();this.loadSummary()}})
new Vue({
el: '#app',
mixins: [initTableMixins],
data() {
return {
year: new Date().getFullYear().toString(),
summaryLoading: false,
feedbackLoading: false,
travelAgencyCompositeRatings: [],
schemeServiceRatings: [],
schemeType: '线路',
selectedSchemeId: '',
feedbackDetails: [],
feedbackType: '全部',
feedbackPage: 1,
feedbackPageSize: 10,
feedbackDetail: {},
feedbackDetailVisible: false,
schemeFeedbackVisible: false
}
},
methods: {
doSearch() {
this.getSatisfactionRatingStatistics()
this.getSatisfactionFeedbackDetails()
},
getSatisfactionRatingStatistics() {
this.$set(this, 'summaryLoading', true)
this.$axios.post(loc() + '/getSatisfactionRatingStatistics', {year: this.year}).then((res) => {
if (res.code === 0) {
const statisticData = res.data || {}
this.$set(this, 'travelAgencyCompositeRatings', statisticData.travelAgencyCompositeRatings || [])
this.$set(this, 'schemeServiceRatings', statisticData.schemeServiceRatings || [])
this.resetSelectedScheme()
} else {
this.$message.warning(res.msg)
}
}).finally(() => {
this.$set(this, 'summaryLoading', false)
})
},
getSatisfactionFeedbackDetails() {
this.$set(this, 'feedbackLoading', true)
this.$axios.post(loc() + '/getSatisfactionFeedbackDetails', {year: this.year}).then((res) => {
if (res.code === 0) {
this.$set(this, 'feedbackDetails', res.data || [])
this.$set(this, 'feedbackPage', 1)
} else {
this.$message.warning(res.msg)
}
}).finally(() => {
this.$set(this, 'feedbackLoading', false)
})
},
formatRatingScore(score) {
if (score === null || score === undefined || score === '') return '-'
return Number(score).toFixed(1)
},
hasRatingScore(score) {
return score !== null && score !== undefined && score !== ''
},
formatFeedbackScore(score) {
if (score === null || score === undefined || score === '') return '-'
return Number(score).toFixed(1) + ' 分'
},
getFeedbackContentSummary(content) {
const feedbackContent = String(content || '').trim()
if (!feedbackContent) return '暂无评价建议'
return feedbackContent.length > 36 ? feedbackContent.substring(0, 36) + '…' : feedbackContent
},
changeSchemeType(value) {
this.$set(this, 'schemeType', value)
this.resetSelectedScheme()
},
selectScheme(schemeRating) {
this.$set(this, 'selectedSchemeId', schemeRating.schemeId)
},
isSelectedScheme(schemeRating) {
return String(this.selectedSchemeId) === String(schemeRating.schemeId)
},
getSchemeRatingsByType() {
return this.schemeServiceRatings.filter((item) => item.typeName === this.schemeType)
},
getSelectedSchemeRating() {
return this.getSchemeRatingsByType().find((item) => this.isSelectedScheme(item))
},
resetSelectedScheme() {
const schemeRatings = this.getSchemeRatingsByType()
this.$set(this, 'selectedSchemeId', schemeRatings.length ? schemeRatings[0].schemeId : '')
},
getSelectedSchemeMetrics() {
const schemeRating = this.getSelectedSchemeRating() || {}
const metrics = [
{key: 'travelAgency', label: '旅行社', icon: 'el-icon-guide', avg: schemeRating.travelAgencyAvg, count: schemeRating.travelAgencyCount},
{key: 'journey', label: '行程', icon: 'el-icon-map-location', avg: schemeRating.journeyAvg, count: schemeRating.journeyCount},
{key: 'accommodation', label: '住宿', icon: 'el-icon-house', avg: schemeRating.accommodationAvg, count: schemeRating.accommodationCount},
{key: 'dining', label: '餐饮', icon: 'el-icon-food', avg: schemeRating.diningAvg, count: schemeRating.diningCount},
{key: 'transportation', label: '交通', icon: 'el-icon-truck', avg: schemeRating.transportationAvg, count: schemeRating.transportationCount}
]
if (schemeRating.typeName === '线路') return metrics
return [
metrics[0],
{key: 'accommodation', label: '酒店', icon: 'el-icon-house', avg: schemeRating.accommodationAvg, count: schemeRating.accommodationCount}
]
},
getSelectedSchemeFeedbackDetails() {
const schemeRating = this.getSelectedSchemeRating()
if (!schemeRating) return []
return this.feedbackDetails.filter((item) => item.typeName === schemeRating.typeName
&& String(item.schemeId) === String(schemeRating.schemeId)
&& String(item.feedbackContent || '').trim())
},
openSchemeFeedbackDialog() {
this.$set(this, 'schemeFeedbackVisible', true)
},
closeSchemeFeedbackDialog() {
this.$set(this, 'schemeFeedbackVisible', false)
},
changeFeedbackType(value) {
this.$set(this, 'feedbackType', value)
this.$set(this, 'feedbackPage', 1)
},
exportFeedbackDetails() {
this.$downLoad(loc() + '/exportSatisfactionFeedbackDetails', {
year: this.year,
typeName: this.feedbackType
})
},
getFilteredFeedbackDetails() {
if (this.feedbackType === '全部') return this.feedbackDetails
return this.feedbackDetails.filter((item) => item.typeName === this.feedbackType)
},
getPagedFeedbackDetails() {
const start = (this.feedbackPage - 1) * this.feedbackPageSize
return this.getFilteredFeedbackDetails().slice(start, start + this.feedbackPageSize)
},
feedbackIndexMethod(index) {
return (this.feedbackPage - 1) * this.feedbackPageSize + index + 1
},
changeFeedbackPage(page) {
this.$set(this, 'feedbackPage', page)
},
changeFeedbackPageSize(pageSize) {
this.$set(this, 'feedbackPageSize', pageSize)
this.$set(this, 'feedbackPage', 1)
},
openFeedbackDetail(feedbackDetail) {
this.$set(this, 'feedbackDetail', feedbackDetail)
this.$set(this, 'feedbackDetailVisible', true)
},
closeFeedbackDetail() {
this.$set(this, 'feedbackDetailVisible', false)
this.$set(this, 'feedbackDetail', {})
}
},
created() {
this.doSearch()
}
})
</script>
<!--# } #-->
<!--#
}
#-->
@@ -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>