This commit is contained in:
=
2026-06-01 15:31:22 +08:00
parent ab29205a32
commit 41b370692e
20 changed files with 1175 additions and 133 deletions
@@ -68,11 +68,11 @@ public class NutShiroProcessor extends AbstractProcessor {
//不需要认证授权的url
String[] authIgnoreUrlArr = Lang.array("/",
"/sso/login",
// "/platform/login",
"/platform/login",
"/platform/login(/doLogin|/logout|/captcha)",
"/platform/qywechat/.*",
"/platform/home/(500|403|404|UnknownAccountError|LockedAccountError)",
// "/mobile/login",
"/mobile/login",
"/mobile/login/doLogin",
"/platform/jsz/login",
"/platform/activity/basic/scope/getScopeUser",
@@ -3,6 +3,7 @@ package io.v.nutz.zhgh.activity.controller.basic;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.hutool.core.util.StrUtil;
import cn.wizzer.framework.base.Result;
import io.v.nutz.base.utils.ViTool;
@@ -31,9 +32,12 @@ import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.AdaptBy;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.POST;
import org.nutz.mvc.annotation.Param;
import org.nutz.mvc.upload.TempFile;
import org.nutz.mvc.upload.UploadAdaptor;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@@ -105,7 +109,8 @@ public class ActivityBasicScopeController {
@Param(value = "clubId", required = false) String clubId,
@Param(value = "reverseSelection") boolean reverseSelection,
@Param(value = "activityGroupId", required = false) Integer activityGroupId,
@Param(value = "activityUserCnd", required = false) String activityUserCndStr) {
@Param(value = "activityUserCnd", required = false) String activityUserCndStr,
@Param(value = "existsLoginNameRedisKey", required = false) String existsLoginNameRedisKey) {
Sql sql = Sqls.create("""
SELECT
DISTINCT(u.id) as id,
@@ -127,6 +132,7 @@ public class ActivityBasicScopeController {
""");
try {
Cnd cnd = getCnd(pageForm, unionId, unitId, personTypes, userStates, memberTypes, sexTypes, age, teacherMeetingId, roleIds, userId, clubId, reverseSelection, activityGroupId, activityUserCndStr);
activityBasicScopeService.appendImportUserCnd(cnd, existsLoginNameRedisKey);
sql.setCondition(cnd);
@@ -169,7 +175,8 @@ public class ActivityBasicScopeController {
@Param(value = "userId", required = false) String[] userId,
@Param(value = "clubId", required = false) String clubId,
@Param(value = "reverseSelection") boolean reverseSelection,
@Param(value = "activityUserCnd", required = false) String activityUserCndStr) {
@Param(value = "activityUserCnd", required = false) String activityUserCndStr,
@Param(value = "existsLoginNameRedisKey", required = false) String existsLoginNameRedisKey) {
Sql sql = Sqls.create("""
SELECT DISTINCT
( u.id ) AS userId
@@ -180,6 +187,7 @@ public class ActivityBasicScopeController {
$condition
""");
Cnd cnd = getCnd(pageForm, unionId, unitId, personTypes, userStates, memberTypes, sexTypes, age, teacherMeetingId, roleIds, userId, clubId, reverseSelection, activityGroupId, activityUserCndStr);
activityBasicScopeService.appendImportUserCnd(cnd, existsLoginNameRedisKey);
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.entities());
@@ -322,7 +330,7 @@ public class ActivityBasicScopeController {
}
@At
@ViReturn
@Ok("void")
@RequiresPermissions("activity.basic.scope")
public void doExportUser(PageForm pageForm,
@Param(value = "props", required = false) String props,
@@ -339,7 +347,9 @@ public class ActivityBasicScopeController {
@Param(value = "clubId", required = false) String clubId,
@Param(value = "reverseSelection") boolean reverseSelection,
@Param(value = "activityGroupId", required = false) Integer activityGroupId,
@Param(value = "activityUserCnd", required = false) String activityUserCndStr, HttpServletResponse response) {
@Param(value = "activityUserCnd", required = false) String activityUserCndStr,
@Param(value = "existsLoginNameRedisKey", required = false) String existsLoginNameRedisKey,
HttpServletResponse response) {
Sql sql = Sqls.create("""
SELECT
@@ -363,6 +373,7 @@ public class ActivityBasicScopeController {
try {
Cnd cnd = getCnd(pageForm, unionId, unitId, personTypes, userStates, memberTypes, sexTypes, age, teacherMeetingId, roleIds, userId, clubId, reverseSelection, activityGroupId, activityUserCndStr);
activityBasicScopeService.appendImportUserCnd(cnd, existsLoginNameRedisKey);
sql.setCondition(cnd);
@@ -377,14 +388,50 @@ public class ActivityBasicScopeController {
entityList.add(new ExcelExportEntity(v, k, 40));
});
ViTool.excelResponse(response, "人员名单.xls");
Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), entityList, map);
ViTool.excelResponse(response, "人员名单.xlsx");
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entityList, map);
workbook.write(response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
@At
@Ok("void")
@RequiresPermissions("activity.basic.scope")
public void downloadImport(HttpServletResponse response) {
try {
ViTool.excelResponse(response, "人员导入模板.xlsx");
List<ExcelExportEntity> entityList = new ArrayList<>();
entityList.add(new ExcelExportEntity("工号", "loginname", 20));
entityList.add(new ExcelExportEntity("姓名", "username", 20));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entityList, new ArrayList<>());
workbook.write(response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
@At
@ViReturn
@RequiresPermissions("activity.basic.scope")
public Object clearSearchCnd(String existsLoginNameRedisKey) {
activityBasicScopeService.clearImportUserCnd(existsLoginNameRedisKey);
return null;
}
@At
@ViReturn
@AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"})
@RequiresPermissions("activity.basic.scope")
public Object doImport(TempFile file) {
return activityBasicScopeService.checkImportUser(file.getFile());
}
private Cnd getCnd(PageForm pageForm,
String unionId,
@@ -0,0 +1,18 @@
package io.v.nutz.zhgh.activity.controller.template;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
@Data
public class UserTemp {
@Excel(name = "工号")
private String loginname;
@Excel(name = "姓名")
private String username;
@Excel(name = "备注")
private String remarks;
}
@@ -4,7 +4,9 @@ import io.v.nutz.zhgh.activity.models.ActivityUserScope;
import io.v.nutz.base.service.ViService;
import org.nutz.dao.Cnd;
import org.nutz.dao.sql.Sql;
import org.nutz.lang.util.NutMap;
import java.io.File;
import java.util.List;
/**
@@ -21,4 +23,27 @@ public interface ActivityBasicScopeService extends ViService<ActivityUserScope>
*/
void largeDataInsert(List<ActivityUserScope> list) ;
/**
* 解析人员导入模板,核对系统中存在的工号,并把匹配到的工号暂存为后续查询条件。
*
* @param file 上传的人员导入 Excel 文件
* @return 导入总数、成功数、错误数、错误明细和临时查询 key
*/
NutMap checkImportUser(File file);
/**
* 将导入核对成功的工号追加到当前查询条件中。
*
* @param cnd 查询条件
* @param existsLoginNameRedisKey 导入核对成功工号的临时缓存 key
*/
void appendImportUserCnd(Cnd cnd, String existsLoginNameRedisKey);
/**
* 清理导入人员临时查询条件。
*
* @param existsLoginNameRedisKey 导入核对成功工号的临时缓存 key
*/
void clearImportUserCnd(String existsLoginNameRedisKey);
}
@@ -1,24 +1,37 @@
package io.v.nutz.zhgh.activity.services.impl;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import io.v.nutz.zhgh.activity.models.ActivityUserScope;
import io.v.nutz.zhgh.activity.services.ActivityBasicScopeService;
import io.v.nutz.base.utils.DateUtil;
import io.v.nutz.base.service.impl.ViServiceImpl;
import io.v.nutz.zhgh.activity.controller.template.UserTemp;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.impl.NutTxDao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* @author zxy
@@ -28,6 +41,11 @@ import java.util.concurrent.atomic.AtomicInteger;
@IocBean(args = {"refer:dao"})
@Slf4j
public class ActivityBasicScopeServiceImpl extends ViServiceImpl<ActivityUserScope> implements ActivityBasicScopeService {
private static final String IMPORT_NO_MATCH_LOGIN_NAME = "__activity_scope_import_no_match__";
@Inject
private RedisService redisService;
public ActivityBasicScopeServiceImpl(Dao dao) {
super(dao);
}
@@ -79,4 +97,109 @@ public class ActivityBasicScopeServiceImpl extends ViServiceImpl<ActivityUserSco
}
log.info("结束时间" + DateUtil.getDateTime());
}
/**
* 解析导入模板并缓存匹配到的工号,缓存 key 会返回给前端继续用于查询和分组设置。
*
* @param file 上传的人员导入 Excel 文件
* @return 导入核对结果
*/
@Override
public NutMap checkImportUser(File file) {
String matchUserLoginNamesKey = "ActivityBasicScopeService.checkImportUser.time=" + System.currentTimeMillis();
List<UserTemp> userImportList = ExcelImportUtil.importExcel(file, UserTemp.class, new ImportParams());
if (userImportList == null) {
userImportList = new ArrayList<>();
}
Set<String> sysLoginNames = getSysLoginNames();
List<String> existsLoginNames = new ArrayList<>();
for (UserTemp excelUser : userImportList) {
if (excelUser == null || StrUtil.isBlank(excelUser.getLoginname())) {
if (excelUser != null) {
excelUser.setRemarks("工号不能为空");
}
continue;
}
if (sysLoginNames.contains(excelUser.getLoginname())) {
existsLoginNames.add(excelUser.getLoginname());
} else {
excelUser.setRemarks("系统查不到此人");
}
}
List<UserTemp> errorExcelTempUsers = userImportList.stream()
.filter(v -> v != null && StrUtil.isNotBlank(v.getRemarks()))
.collect(Collectors.toList());
NutMap nutMap = NutMap.NEW();
nutMap.setv("totalCount", userImportList.size());
nutMap.setv("successCount", existsLoginNames.size());
nutMap.setv("errorCount", errorExcelTempUsers.size());
nutMap.setv("errorList", errorExcelTempUsers.stream().map(v -> NutMap.NEW()
.addv("工号", v.getLoginname())
.addv("姓名", v.getUsername())
.addv("错误原因", v.getRemarks())).collect(Collectors.toList()));
nutMap.setv("existsLoginNameRedisKey", matchUserLoginNamesKey);
// 没有匹配人员时写入一个不可能存在的工号,避免后续查询误变成全量。
String[] cacheLoginNames = Lang.isNotEmpty(existsLoginNames)
? existsLoginNames.toArray(new String[0])
: new String[]{IMPORT_NO_MATCH_LOGIN_NAME};
redisService.lpush(matchUserLoginNamesKey, cacheLoginNames);
redisService.expire(matchUserLoginNamesKey, 60 * 60);
return nutMap;
}
/**
* 根据导入核对返回的 Redis key,给现有查询条件追加工号范围。
*
* @param cnd 查询条件
* @param existsLoginNameRedisKey 导入核对成功工号的临时缓存 key
*/
@Override
public void appendImportUserCnd(Cnd cnd, String existsLoginNameRedisKey) {
if (StrUtil.isBlank(existsLoginNameRedisKey)) {
return;
}
List<String> loginNames = redisService.lrange(existsLoginNameRedisKey, 0, -1);
if (Lang.isEmpty(loginNames)) {
cnd.and("u.loginname", "=", IMPORT_NO_MATCH_LOGIN_NAME);
return;
}
cnd.andEX("u.loginname", "in", loginNames);
}
/**
* 清理导入核对成功工号的临时缓存。
*
* @param existsLoginNameRedisKey 导入核对成功工号的临时缓存 key
*/
@Override
public void clearImportUserCnd(String existsLoginNameRedisKey) {
if (StrUtil.isNotBlank(existsLoginNameRedisKey)) {
redisService.del(existsLoginNameRedisKey);
}
}
private Set<String> getSysLoginNames() {
Sql sql = Sqls.queryString("""
SELECT
u.loginname
FROM
`user` u
""");
dao().execute(sql);
String[] sysLoginNames = (String[]) sql.getResult();
Set<String> loginNameSet = new HashSet<>();
if (sysLoginNames != null) {
for (String loginName : sysLoginNames) {
if (StrUtil.isNotBlank(loginName)) {
loginNameSet.add(loginName);
}
}
}
return loginNameSet;
}
}
@@ -21,6 +21,7 @@ import io.v.nutz.base.utils.ViTool;
import io.v.nutz.sys.models.SysHoliday;
import io.v.nutz.sys.models.User;
import io.v.nutz.web.commons.utils.ShiroUtil;
import io.v.nutz.zhgh.therapyRecuperation.constant.TheRapyRecuperationState;
import io.v.nutz.zhgh.therapyRecuperation.model.*;
import io.v.nutz.zhgh.therapyRecuperation.service.baseManage.TheRapyRecuperationBaseManagerService;
import org.apache.poi.ss.usermodel.Workbook;
@@ -117,6 +118,7 @@ public class TheRapyRecuperationBaseManagerController {
tb.activityStartTime,
tb.activityEndTime,
tb.files,
tb.maxSignCount,
tb.baseContactPerson,
tb.baseContactNumber,
tb.files as fileId,
@@ -286,7 +288,11 @@ public class TheRapyRecuperationBaseManagerController {
return Result.error("参数错误");
}
TheRapyRecuperationConfig config = dao.fetch(TheRapyRecuperationConfig.class, Cnd.NEW());
// 手机端日历只使用最新提交配置,保持活动时间和页面展示一致。
TheRapyRecuperationConfig config = dao.fetch(TheRapyRecuperationConfig.class, Cnd.where("latestConfig", "=", true));
if (config == null) {
return Result.error("未找到最新疗休养配置,请联系管理员");
}
//获取线路对应的标段
TheRapyRecuperationBaseManagement management = dao.fetch(TheRapyRecuperationBaseManagement.class, id);
//TheRapyRecuperationLot lot = dao.fetch(TheRapyRecuperationLot.class, management.getLotId());
@@ -306,6 +312,7 @@ public class TheRapyRecuperationBaseManagerController {
});
List<Integer> weeks = management.getWeekCheckIn();
Set<String> fullDays = getFullBaseManagementDays(management);
// 解析日期
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@@ -330,6 +337,10 @@ public class TheRapyRecuperationBaseManagerController {
currentDate = currentDate.plusDays(1);
continue;
}
if (fullDays.contains(currentDateString)) {
currentDate = currentDate.plusDays(1);
continue;
}
// 检查是否为需要排除的周几
int dayOfWeek = currentDate.getDayOfWeek().getValue() % 7; // 1是周一,7是周日
if (Lang.isNotEmpty(weeks) && !weeks.contains(dayOfWeek)) {
@@ -343,11 +354,77 @@ public class TheRapyRecuperationBaseManagerController {
return Map.of("days", resultDates,
"holidays", holidays,
"fullDays", fullDays,
"lotValue", 0,
"minDate", DateUtil.formatDate(config.getFragmentPlayStartTime()),
"maxDate", DateUtil.formatDate(config.getFragmentPlayEndTime()));
}
/**
* 获取指定酒店报名已满的日期,手机端日历用来过滤不可选入住日。
*
* @param management 酒店信息
* @return 已达到每日最高报名人数的日期集合
*/
private Set<String> getFullBaseManagementDays(TheRapyRecuperationBaseManagement management) {
if (management == null || management.getMaxSignCount() == null || management.getMaxSignCount() <= 0) {
return Collections.emptySet();
}
List<TheRapyRecuperationEnroll> enrolls = dao.query(TheRapyRecuperationEnroll.class,
Cnd.where("takePartInBaseManagementId", "=", management.getId())
.and("specificTime", "is not", null)
.and("isNormal", "=", true)
.and("stateId", "in", Lang.array(TheRapyRecuperationState.UNIT, TheRapyRecuperationState.LINEUNIT, TheRapyRecuperationState.SCHOOL, TheRapyRecuperationState.PASS))
.and("YEAR(signingUptime)", "=", DateUtil.thisYear()));
Map<String, Integer> dayCountMap = new HashMap<>();
for (TheRapyRecuperationEnroll enroll : enrolls) {
for (String day : parseSpecificTimeDays(enroll.getSpecificTime())) {
dayCountMap.put(day, dayCountMap.getOrDefault(day, 0) + 1);
}
}
return dayCountMap.entrySet().stream()
.filter(entry -> entry.getValue() >= management.getMaxSignCount())
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
/**
* 把“06月01日-06月03日”格式的报名时间拆成逐日日期。
*
* @param specificTime 报名时间段
* @return 报名时间段覆盖的日期集合,格式 yyyy-MM-dd
*/
private List<String> parseSpecificTimeDays(String specificTime) {
if (StrUtil.isBlank(specificTime) || !specificTime.contains("-")) {
return Collections.emptyList();
}
String[] times = specificTime.split("-");
if (times.length != 2 || StrUtil.isBlank(times[0]) || StrUtil.isBlank(times[1])) {
return Collections.emptyList();
}
DateTimeFormatter sourceFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
DateTimeFormatter targetFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate start;
LocalDate end;
try {
start = LocalDate.parse(DateUtil.thisYear() + "" + times[0].trim(), sourceFormatter);
end = LocalDate.parse(DateUtil.thisYear() + "" + times[1].trim(), sourceFormatter);
} catch (Exception e) {
return Collections.emptyList();
}
if (end.isBefore(start)) {
end = end.plusYears(1);
}
List<String> days = new ArrayList<>();
LocalDate current = start;
while (!current.isAfter(end)) {
days.add(current.format(targetFormatter));
current = current.plusDays(1);
}
return days;
}
/**
* 目的地导入模版
@@ -37,10 +37,12 @@ import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.upload.TempFile;
import org.nutz.mvc.upload.UploadAdaptor;
import org.apache.commons.io.IOUtils;
import org.springframework.util.CollectionUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -183,6 +185,30 @@ public class TheRapyRecuperationUserQueryController {
}
/**
* 下载参加人员导入模板
*
* @param response 响应
*/
@At
@Ok("void")
@RequiresAuthentication
public void downloadImportTemplate(HttpServletResponse response) {
String templatePath = "templates/travel/TravelEnrollImport.xlsx";
try (InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(templatePath)) {
if (inputStream == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND, "模板文件不存在");
return;
}
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + new String(("参加人员导入模板.xlsx").getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
IOUtils.copy(inputStream, response.getOutputStream());
} catch (Exception e) {
log.error(e.getMessage());
}
}
/**
* 参加人员导入
*
@@ -94,7 +94,10 @@ public class TheRapyRecuperationSchoolUnionUserQueryController {
state.stateColor,
line.regionalNature,
line.lineName,
agency.travelAgencyName,
COALESCE(agency.travelAgencyName, lineAgency.travelAgencyName) AS travelAgencyName,
lot.lotName,
lot.activityCost,
CASE WHEN lineu.playStartTime IS NULL AND lineu.playEndTime IS NULL THEN '' ELSE CONCAT(IFNULL(DATE_FORMAT(lineu.playStartTime,'%Y-%m-%d'), ''), '至', IFNULL(DATE_FORMAT(lineu.playEndTime,'%Y-%m-%d'), '')) END AS travelTime,
ma.baseName,
enroll.*,
lineu.lineId,
@@ -107,6 +110,8 @@ public class TheRapyRecuperationSchoolUnionUserQueryController {
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 = enroll.takePartInTravelAgencyId
LEFT JOIN the_rapy_recuperation_travel_agency lineAgency ON lineAgency.id = line.travelAgencyId
LEFT JOIN the_rapy_recuperation_lot lot ON lot.id = line.lotId
LEFT JOIN the_rapy_recuperation_base_management ma on ma.id = enroll.takePartInBaseManagementId
LEFT JOIN audit_state state ON state.stateId = enroll.stateId
$condition
@@ -293,7 +298,10 @@ public class TheRapyRecuperationSchoolUnionUserQueryController {
state.stateColor,
line.regionalNature,
line.lineName,
agency.travelAgencyName,
COALESCE(agency.travelAgencyName, lineAgency.travelAgencyName) AS travelAgencyName,
lot.lotName,
lot.activityCost,
CASE WHEN lineu.playStartTime IS NULL AND lineu.playEndTime IS NULL THEN '' ELSE CONCAT(IFNULL(DATE_FORMAT(lineu.playStartTime,'%Y-%m-%d'), ''), '至', IFNULL(DATE_FORMAT(lineu.playEndTime,'%Y-%m-%d'), '')) END AS travelTime,
ma.baseName,
enroll.*,
lineu.lineId,
@@ -306,6 +314,8 @@ public class TheRapyRecuperationSchoolUnionUserQueryController {
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 = enroll.takePartInTravelAgencyId
LEFT JOIN the_rapy_recuperation_travel_agency lineAgency ON lineAgency.id = line.travelAgencyId
LEFT JOIN the_rapy_recuperation_lot lot ON lot.id = line.lotId
LEFT JOIN the_rapy_recuperation_base_management ma on ma.id = enroll.takePartInBaseManagementId
LEFT JOIN audit_state state ON state.stateId = enroll.stateId
$condition
@@ -378,6 +388,12 @@ public class TheRapyRecuperationSchoolUnionUserQueryController {
excelEntities.add(new ExcelExportEntity("工会", "unionName", 20));
excelEntities.add(new ExcelExportEntity("身份证号", "idCard", 30));
excelEntities.add(new ExcelExportEntity("手机号", "mobile", 20));
// 导出线路关联信息,与列表查询使用同一批筛选条件,确保列表查到什么导出什么。
excelEntities.add(new ExcelExportEntity("出行时间", "travelTime", 30));
excelEntities.add(new ExcelExportEntity("标段", "lotName", 20));
excelEntities.add(new ExcelExportEntity("标段费用", "activityCost", 20));
excelEntities.add(new ExcelExportEntity("线路", "lineName", 30));
excelEntities.add(new ExcelExportEntity("旅行社", "travelAgencyName", 30));
if (config.getFamilyInfo() == 2) {
excelEntities.add(new ExcelExportEntity("与本人关系", "relation", 10));
excelEntities.add(new ExcelExportEntity("床型", "bedType", 10));
@@ -1,32 +1,14 @@
package io.v.nutz.zhgh.therapyRecuperation.controller.theRapyConfig;
import io.v.nutz.base.annontation.ViReturn;
import io.v.nutz.base.model.msgUser;
import io.v.nutz.base.utils.MsgApi;
import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationBaseManagement;
import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationConfig;
import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationLine;
import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationLot;
import io.v.nutz.zhgh.therapyRecuperation.service.TheRapyRecuperationConfigService;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.trans.Trans;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* rapy休养配置控制器
@@ -41,9 +23,7 @@ public class TheRapyRecuperationConfigController {
@Inject
private Dao dao;
@Inject
private MsgApi msgApi;
private TheRapyRecuperationConfigService configService;
@At("")
@@ -55,29 +35,23 @@ public class TheRapyRecuperationConfigController {
@At
@ViReturn
@RequiresAuthentication
@Aop(TransAop.READ_COMMITTED)
public Object operation(TheRapyRecuperationConfig config, String[] lotDeleteList) {
if (Strings.isNotBlank(config.getId())) {
if (Lang.isNotEmpty(lotDeleteList)) {
dao.clear(TheRapyRecuperationLot.class, Cnd.where("id", "in", lotDeleteList));
}
dao.updateWith(config, "lots");
List<TheRapyRecuperationLot> collect = config.getLots().stream().filter(v -> v.getConfigId() == null).collect(Collectors.toList());
for (TheRapyRecuperationLot obj : collect) {
obj.setConfigId(config.getId());
}
dao.insert(collect);
} else {
dao.insertWith(config, "lots");
}
configService.saveConfig(config, lotDeleteList);
return null;
}
@At
@ViReturn
@RequiresAuthentication
public Object findOne() {
return dao.fetchLinks(dao.fetch(TheRapyRecuperationConfig.class), "lots",Cnd.NEW().desc("lotValue"));
public Object findOne(Integer year) {
return configService.findByYear(year);
}
@At
@ViReturn
@RequiresAuthentication
public Object copyLastYear(Integer year) {
return configService.copyLastYear(year);
}
@@ -90,20 +64,7 @@ public class TheRapyRecuperationConfigController {
@ViReturn
@RequiresAuthentication
public Object getLotsById(String lotId){
List<String> lineNames = new ArrayList<>();
List<String> baseNames = new ArrayList<>();
Map<String, List<String>> map = new HashMap<>();
List<TheRapyRecuperationLine> lineList = dao.query(TheRapyRecuperationLine.class, Cnd.where("lotId", "=", lotId));
if (!CollectionUtils.isEmpty(lineList)){
lineList.forEach(v-> lineNames.add(v.getLineName()));
map.put("line",lineNames);
}
List<TheRapyRecuperationBaseManagement> managementList = dao.query(TheRapyRecuperationBaseManagement.class, Cnd.where("lotId", "=", lotId));
if (!CollectionUtils.isEmpty(managementList)){
managementList.forEach(v-> baseNames.add(v.getBaseName()));
map.put("base",baseNames);
}
return map;
return configService.getLotsById(lotId);
}
@@ -116,19 +77,7 @@ public class TheRapyRecuperationConfigController {
@ViReturn
@RequiresAuthentication
public Object deleteLotById(String lotId){
Trans.exec(()->{
dao.clear(TheRapyRecuperationLot.class, Cnd.where("id","=",lotId));
List<TheRapyRecuperationLine> lineList = dao.query(TheRapyRecuperationLine.class, Cnd.where("lotId", "=", lotId));
if (!CollectionUtils.isEmpty(lineList)) {
lineList.forEach(v -> v.setLotId(null));
dao.update(lineList);
}
List<TheRapyRecuperationBaseManagement> managementList = dao.query(TheRapyRecuperationBaseManagement.class, Cnd.where("lotId", "=", lotId));
if (!CollectionUtils.isEmpty(managementList)) {
managementList.forEach(v -> v.setLotId(null));
dao.update(managementList);
}
});
configService.deleteLotById(lotId);
return null;
}
}
@@ -165,6 +165,12 @@ public class TheRapyRecuperationBaseManagement {
@Comment("天数")
private List<String> allowDay;
@Column
@ColDefine(type = ColType.INT)
@Comment("同一酒店每日最高报名人数")
@Excel(name = "同一酒店每日最高报名人数")
private Integer maxSignCount;
@Column
@ColDefine(type = ColType.VARCHAR)
@Comment("旅行社地点")
@@ -26,6 +26,16 @@ public class TheRapyRecuperationConfig {
@Comment("配置名称")
private String configName;
@Column
@ColDefine(type = ColType.INT)
@Comment("配置年度")
private Integer configYear;
@Column
@ColDefine(type = ColType.BOOLEAN)
@Comment("是否最新提交配置")
private Boolean latestConfig;
@Column
@ColDefine(type = ColType.INT, width = 32)
@Comment("参加人员范围")
@@ -0,0 +1,50 @@
package io.v.nutz.zhgh.therapyRecuperation.service;
import io.v.nutz.base.service.ViService;
import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationConfig;
import org.nutz.lang.util.NutMap;
/**
* 疗休养基础配置服务。
*/
public interface TheRapyRecuperationConfigService extends ViService<TheRapyRecuperationConfig> {
/**
* 按年度查询基础配置;年度为空时使用当前年度。
*
* @param configYear 配置年度
* @return 年度配置
*/
TheRapyRecuperationConfig findByYear(Integer configYear);
/**
* 保存年度基础配置,已存在配置时更新,不存在时新增。
*
* @param config 基础配置
* @param lotDeleteList 需要删除的标段id
*/
void saveConfig(TheRapyRecuperationConfig config, String[] lotDeleteList);
/**
* 将上一年度配置复制为当前年度配置。
*
* @param configYear 当前配置年度
* @return 复制后的当前年度配置
*/
TheRapyRecuperationConfig copyLastYear(Integer configYear);
/**
* 查询使用该标段的线路和目的地。
*
* @param lotId 标段id
* @return 使用情况
*/
NutMap getLotsById(String lotId);
/**
* 强制删除标段,并清空已绑定线路和目的地上的标段。
*
* @param lotId 标段id
*/
void deleteLotById(String lotId);
}
@@ -0,0 +1,236 @@
package io.v.nutz.zhgh.therapyRecuperation.service.impl;
import cn.hutool.core.date.DateTime;
import io.v.nutz.base.service.impl.ViServiceImpl;
import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationBaseManagement;
import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationConfig;
import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationLine;
import io.v.nutz.zhgh.therapyRecuperation.model.TheRapyRecuperationLot;
import io.v.nutz.zhgh.therapyRecuperation.service.TheRapyRecuperationConfigService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import org.nutz.trans.Trans;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 疗休养基础配置服务实现。
*/
@IocBean(args = {"refer:dao"})
public class TheRapyRecuperationConfigServiceImpl extends ViServiceImpl<TheRapyRecuperationConfig> implements TheRapyRecuperationConfigService {
private static final int DEFAULT_CONFIG_YEAR = 2025;
public TheRapyRecuperationConfigServiceImpl(Dao dao) {
super(dao);
}
/**
* 年度为空时统一使用 2025 年,兼容原有无年度的基础配置。
*
* @param configYear 配置年度
* @return 有效年度
*/
private Integer getConfigYear(Integer configYear) {
return configYear == null ? DEFAULT_CONFIG_YEAR : configYear;
}
@Override
public TheRapyRecuperationConfig findByYear(Integer configYear) {
if (configYear == null) {
return findLatestConfig();
}
Integer year = getConfigYear(configYear);
TheRapyRecuperationConfig config = fetch(Cnd.where("configYear", "=", year));
if (config == null && year == DEFAULT_CONFIG_YEAR) {
config = fetch(Cnd.where("configYear", "is", null));
}
if (config == null) {
return null;
}
return dao().fetchLinks(config, "lots", Cnd.NEW().desc("lotValue"));
}
/**
* 其他业务页面不传年度时,只使用最新提交的基础配置。
*
* @return 最新基础配置
*/
private TheRapyRecuperationConfig findLatestConfig() {
TheRapyRecuperationConfig config = fetch(Cnd.where("latestConfig", "=", true));
if (config == null) {
return null;
}
return dao().fetchLinks(config, "lots", Cnd.NEW().desc("lotValue"));
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void saveConfig(TheRapyRecuperationConfig config, String[] lotDeleteList) {
Integer year = getConfigYear(config.getConfigYear());
config.setConfigYear(year);
config.setLatestConfig(true);
TheRapyRecuperationConfig oldConfig = findByYear(year);
if (Strings.isBlank(config.getId()) && oldConfig != null) {
config.setId(oldConfig.getId());
}
resetLatestConfig(config.getId());
if (Strings.isNotBlank(config.getId())) {
if (Lang.isNotEmpty(lotDeleteList)) {
dao().clear(TheRapyRecuperationLot.class, Cnd.where("id", "in", lotDeleteList));
}
dao().updateWith(config, "lots");
List<TheRapyRecuperationLot> lots = config.getLots() == null ? new ArrayList<>() : config.getLots();
List<TheRapyRecuperationLot> collect = lots.stream().filter(v -> v.getConfigId() == null).collect(Collectors.toList());
for (TheRapyRecuperationLot obj : collect) {
obj.setConfigId(config.getId());
}
if (Lang.isNotEmpty(collect)) {
dao().insert(collect);
}
} else {
dao().insertWith(config, "lots");
}
}
/**
* 每次提交年度配置后,仅保留当前配置为最新配置。
*
* @param currentConfigId 当前配置id
*/
private void resetLatestConfig(String currentConfigId) {
Cnd cnd = Cnd.NEW();
if (Strings.isNotBlank(currentConfigId)) {
cnd.and("id", "!=", currentConfigId);
}
dao().update(TheRapyRecuperationConfig.class, Chain.make("latestConfig", false), cnd);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public TheRapyRecuperationConfig copyLastYear(Integer configYear) {
Integer year = getConfigYear(configYear);
TheRapyRecuperationConfig currentConfig = findByYear(year);
TheRapyRecuperationConfig lastConfig = findByYear(year - 1);
if (lastConfig == null) {
return null;
}
lastConfig.setId(currentConfig == null ? null : currentConfig.getId());
lastConfig.setConfigYear(year);
lastConfig.setLatestConfig(true);
resetConfigTimeYear(lastConfig, year);
resetLatestConfig(null);
if (currentConfig == null) {
copyLotsToNewConfig(lastConfig);
dao().insertWith(lastConfig, "lots");
} else {
dao().clear(TheRapyRecuperationLot.class, Cnd.where("configId", "=", currentConfig.getId()));
copyLotsToNewConfig(lastConfig);
dao().update(lastConfig);
for (TheRapyRecuperationLot lot : lastConfig.getLots()) {
lot.setConfigId(currentConfig.getId());
}
if (Lang.isNotEmpty(lastConfig.getLots())) {
dao().insert(lastConfig.getLots());
}
}
return findByYear(year);
}
/**
* 沿用上一年度配置时,将所有年度相关时间改为当前配置年度,月日和时分秒保持不变。
*
* @param config 待复制配置
* @param year 当前配置年度
*/
private void resetConfigTimeYear(TheRapyRecuperationConfig config, Integer year) {
config.setCreateLineStartTime(resetDateTimeYear(config.getCreateLineStartTime(), year));
config.setCreateLineEndTime(resetDateTimeYear(config.getCreateLineEndTime(), year));
config.setImplodeStartTime(resetDateTimeYear(config.getImplodeStartTime(), year));
config.setImplodeEndTime(resetDateTimeYear(config.getImplodeEndTime(), year));
config.setFragmentStartTime(resetDateTimeYear(config.getFragmentStartTime(), year));
config.setFragmentEndTime(resetDateTimeYear(config.getFragmentEndTime(), year));
config.setFragmentPlayStartTime(resetDateTimeYear(config.getFragmentPlayStartTime(), year));
config.setFragmentPlayEndTime(resetDateTimeYear(config.getFragmentPlayEndTime(), year));
}
/**
* 替换时间年份;闰年 2 月 29 日复制到平年时,自动落到该年 2 月最后一天。
*
* @param dateTime 原时间
* @param year 目标年度
* @return 替换年份后的时间
*/
private DateTime resetDateTimeYear(DateTime dateTime, Integer year) {
if (dateTime == null || year == null) {
return dateTime;
}
LocalDateTime sourceTime = LocalDateTime.ofInstant(dateTime.toInstant(), ZoneId.systemDefault());
int dayOfMonth = (int) Math.min(sourceTime.getDayOfMonth(), sourceTime.withYear(year).range(ChronoField.DAY_OF_MONTH).getMaximum());
LocalDateTime targetTime = sourceTime.withYear(year).withDayOfMonth(dayOfMonth);
return new DateTime(java.util.Date.from(targetTime.atZone(ZoneId.systemDefault()).toInstant()));
}
/**
* 复制上一年度标段时清空主键和配置外键,让数据库生成当前年度独立数据。
*
* @param config 待复制配置
*/
private void copyLotsToNewConfig(TheRapyRecuperationConfig config) {
if (config.getLots() == null) {
return;
}
for (TheRapyRecuperationLot lot : config.getLots()) {
lot.setId(null);
lot.setConfigId(null);
}
}
@Override
public NutMap getLotsById(String lotId) {
List<String> lineNames = new ArrayList<>();
List<String> baseNames = new ArrayList<>();
NutMap map = new NutMap();
List<TheRapyRecuperationLine> lineList = dao().query(TheRapyRecuperationLine.class, Cnd.where("lotId", "=", lotId));
if (!CollectionUtils.isEmpty(lineList)) {
lineList.forEach(v -> lineNames.add(v.getLineName()));
map.put("line", lineNames);
}
List<TheRapyRecuperationBaseManagement> managementList = dao().query(TheRapyRecuperationBaseManagement.class, Cnd.where("lotId", "=", lotId));
if (!CollectionUtils.isEmpty(managementList)) {
managementList.forEach(v -> baseNames.add(v.getBaseName()));
map.put("base", baseNames);
}
return map;
}
@Override
public void deleteLotById(String lotId) {
Trans.exec(() -> {
dao().clear(TheRapyRecuperationLot.class, Cnd.where("id", "=", lotId));
List<TheRapyRecuperationLine> lineList = dao().query(TheRapyRecuperationLine.class, Cnd.where("lotId", "=", lotId));
if (!CollectionUtils.isEmpty(lineList)) {
lineList.forEach(v -> v.setLotId(null));
dao().update(lineList);
}
List<TheRapyRecuperationBaseManagement> managementList = dao().query(TheRapyRecuperationBaseManagement.class, Cnd.where("lotId", "=", lotId));
if (!CollectionUtils.isEmpty(managementList)) {
managementList.forEach(v -> v.setLotId(null));
dao().update(managementList);
}
});
}
}
@@ -35,6 +35,7 @@ import org.nutz.lang.util.NutMap;
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@@ -543,8 +544,11 @@ public class TheRapyRecuperationEnrollServiceImpl extends ViServiceImpl<TheRapyR
@Override
public Map<Boolean, String> validSignUpInfo(String loginName, TheRapyRecuperationEnroll enrollInfo) {
//配置信息
TheRapyRecuperationConfig config = dao().fetch(TheRapyRecuperationConfig.class);
// 配置信息只取当前最新配置,避免报名校验和页面展示使用不同年度配置。
TheRapyRecuperationConfig config = fetchLatestConfig();
if (config == null) {
return Map.of(false, "未找到最新疗休养配置,请联系管理员");
}
if (StrUtil.isNotBlank(loginName) && loginName.startsWith("2025")) {
return Map.of(false, "抱歉,您是今年新入职,不能报名!");
@@ -710,7 +714,10 @@ public class TheRapyRecuperationEnrollServiceImpl extends ViServiceImpl<TheRapyR
}
public Map<Boolean, String> validSignTime(TheRapyRecuperationEnroll currentEnroll) {
TheRapyRecuperationConfig config = dao().fetch(TheRapyRecuperationConfig.class, Cnd.NEW());
TheRapyRecuperationConfig config = fetchLatestConfig();
if (config == null) {
return Map.of(false, "未找到最新疗休养配置,请联系管理员");
}
DateTime signUpStartTime = null;
DateTime signUpEndTime = null;
if (StrUtil.isNotBlank(currentEnroll.getTakePartInLineId())) {
@@ -729,7 +736,21 @@ public class TheRapyRecuperationEnrollServiceImpl extends ViServiceImpl<TheRapyR
return null;
}
/**
* 获取最新提交的疗休养配置。
*
* @return 最新疗休养配置
*/
private TheRapyRecuperationConfig fetchLatestConfig() {
return dao().fetch(TheRapyRecuperationConfig.class, Cnd.where("latestConfig", "=", true));
}
public Map<Boolean, String> validBaseManagementCount(String loginName, TheRapyRecuperationEnroll currentEnroll) {
Map<Boolean, String> dailySignCountResult = validBaseManagementDailySignCount(currentEnroll);
if (dailySignCountResult != null && dailySignCountResult.containsKey(false)) {
return dailySignCountResult;
}
if(StrUtil.isBlank(currentEnroll.getId())) {
int count = dao().count(TheRapyRecuperationEnroll.class,
Cnd.where("takePartInBaseManagementId", "=", currentEnroll.getTakePartInBaseManagementId())
@@ -790,6 +811,95 @@ public class TheRapyRecuperationEnrollServiceImpl extends ViServiceImpl<TheRapyR
return null;
}
/**
* 校验分段疗休养同一酒店每日最高报名人数,报名时间段中任意一天满员都不能继续报名。
*
* @param currentEnroll 当前报名信息
* @return 校验失败原因;返回 null 表示未配置上限或未触发限制
*/
public Map<Boolean, String> validBaseManagementDailySignCount(TheRapyRecuperationEnroll currentEnroll) {
if (StrUtil.isBlank(currentEnroll.getTakePartInBaseManagementId())
|| StrUtil.isBlank(currentEnroll.getSpecificTime())) {
return null;
}
TheRapyRecuperationBaseManagement management = dao().fetch(TheRapyRecuperationBaseManagement.class, currentEnroll.getTakePartInBaseManagementId());
if (management == null || management.getMaxSignCount() == null || management.getMaxSignCount() <= 0) {
return null;
}
List<LocalDate> currentDays = parseSpecificTimeDays(currentEnroll.getSpecificTime());
for (LocalDate currentDay : currentDays) {
int signedCount = countBaseManagementSignUserByDay(currentEnroll, currentDay);
if (signedCount + 1 > management.getMaxSignCount()) {
return Map.of(false, "该酒店%s报名人数已满,请重新选择出行时间".formatted(currentDay.format(DateTimeFormatter.ofPattern("MM月dd日"))));
}
}
return null;
}
/**
* 统计同一酒店已报名且出行时间覆盖指定日期的教职工人数。
*
* @param currentEnroll 当前报名信息
* @param currentDay 需要判断的日期
* @return 已覆盖该日期的报名人数
*/
private int countBaseManagementSignUserByDay(TheRapyRecuperationEnroll currentEnroll, LocalDate currentDay) {
Cnd cnd = Cnd.where("takePartInBaseManagementId", "=", currentEnroll.getTakePartInBaseManagementId())
.and("specificTime", "is not", null)
.and("isNormal", "=", true)
.and("stateId", "in", Lang.array(TheRapyRecuperationState.UNIT, TheRapyRecuperationState.LINEUNIT, TheRapyRecuperationState.SCHOOL, TheRapyRecuperationState.PASS))
.and("YEAR(signingUptime)", "=", DateUtil.thisYear());
if (StrUtil.isNotBlank(currentEnroll.getId())) {
cnd.and("id", "!=", currentEnroll.getId());
}
List<TheRapyRecuperationEnroll> enrolls = dao().query(TheRapyRecuperationEnroll.class, cnd);
int count = 0;
for (TheRapyRecuperationEnroll enroll : enrolls) {
List<LocalDate> signDays = parseSpecificTimeDays(enroll.getSpecificTime());
if (signDays.contains(currentDay)) {
count++;
}
}
return count;
}
/**
* 把“06月01日-06月03日”格式的报名时间拆成逐日日期,便于做每日容量判断。
*
* @param specificTime 报名时间段
* @return 报名时间段包含的所有日期
*/
private List<LocalDate> parseSpecificTimeDays(String specificTime) {
if (StrUtil.isBlank(specificTime) || !specificTime.contains("-")) {
return Collections.emptyList();
}
String[] times = specificTime.split("-");
if (times.length != 2 || StrUtil.isBlank(times[0]) || StrUtil.isBlank(times[1])) {
return Collections.emptyList();
}
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
LocalDate start;
LocalDate end;
try {
start = LocalDate.parse(DateUtil.thisYear() + "" + times[0].trim(), formatter);
end = LocalDate.parse(DateUtil.thisYear() + "" + times[1].trim(), formatter);
} catch (Exception e) {
return Collections.emptyList();
}
if (end.isBefore(start)) {
end = end.plusYears(1);
}
List<LocalDate> days = new ArrayList<>();
LocalDate current = start;
while (!current.isAfter(end)) {
days.add(current);
current = current.plusDays(1);
}
return days;
}
//判断一年报几次
public Map<Boolean, String> validCountThisYear(String loginName, int travelFrequency, Boolean validHotel) {
Cnd cnd = Cnd.NEW();
@@ -279,6 +279,10 @@
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="筛选人员">
<template #func>
<el-button v-if="is_sysadmin||is_A06"
type="primary" size="medium" icon="el-icon-printer" @click="openImport">
导入XLSX设置分组
</el-button>
<el-button @click="doExportUser"
size="medium"
type="primary" icon="el-icon-download">
@@ -349,6 +353,57 @@
v-loading="settingLoading"> </el-button>
</span>
</el-dialog>
<el-dialog title="人员导入" :visible.sync="dialogVisible" width="50%" :close-on-click-modal="false" append-to-body>
<el-timeline>
<el-timeline-item timestamp="下载模板" placement="top">
<el-card>
<el-button size="medium" type=""
@click="window.open('/platform/activity/basic/scope/downloadImport')"
icon="el-icon-download">下载模板
</el-button>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="上传文件" placement="top">
<el-card>
<el-upload
name="file"
ref="upload"
:on-remove="handleImportFileRemove"
:on-change="handleImportFileChange"
:auto-upload="false"
:limit="1"
:file-list="importData.fileList">
<el-button size="medium" type="" icon="el-icon-upload"
style="width: 200px">选择文件
</el-button>
<div class="el-upload__tip" slot="tip" style="color: #F56C6C">
只能上传 xls/xlsx 文件
</div>
</el-upload>
</el-card>
</el-timeline-item>
<el-timeline-item placement="top" timestamp="导入结果">
<el-card shadow="never">
<p>总记录数{{ errorInfoData.totalCount }}</p>
<p>成功数<span class="text-success">{{ errorInfoData.successCount }}</span></p>
<p>错误数<span class="text-danger">{{ errorInfoData.errorCount }}</span></p>
<el-link @click="exportErrors" type="primary" v-if="errorInfoData.errorCount>0">下载错误记录
</el-link>
</el-card>
</el-timeline-item>
</el-timeline>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false" type="primary"
:disabled="importLoading"> </el-button>
<el-button type="primary" @click="clearSearchCnd"
:loading="importLoading">清空查询条件</el-button>
<el-button type="primary" @click="doImport" :loading="importLoading">核对人员</el-button>
<el-button type="primary" @click="doImportSearch"
:loading="importLoading">查询人员</el-button>
</span>
</el-dialog>
</div>
</template>
@@ -405,6 +460,7 @@ module.exports = {
roleIds: [],
userId: [],
activityUserCnd: '',
existsLoginNameRedisKey: '',
reverseSelection: false
},
activityUnions: [],
@@ -434,7 +490,18 @@ module.exports = {
setGroupName: [{required: true, message: '请输入分组名称', trigger: ['blur', 'change']}]
},
relatedSessionMenus: ['aca4d14498c145ceb5b24ed70776aae2', '733d7266652740a3aeac9da97ab5eeca', 'fe2d0768e26d4a80beeb159306bb8d01'],
roleData: {}
roleData: {},
dialogVisible: false,
importLoading: false,
importData: {
fileList: [],
},
errorInfoData: {
totalCount: 0,
successCount: 0,
errorCount: 0,
errorList: [],
},
}
},
computed: {
@@ -461,6 +528,88 @@ module.exports = {
'user-cnd': httpVueLoader('/components/plugins/UserCnd.vue'),
},
methods: {
exportErrors() {
const data = this.errorInfoData.errorList
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.json_to_sheet(data)
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
const excelBuffer = XLSX.write(workbook, {bookType: 'xlsx', type: 'array'})
const blob = new Blob([excelBuffer], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '核对记录.xlsx'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
},
doImportSearch() {
this.doSearch()
this.dialogVisible = false
},
doImport() {
if (this.importData.fileList.length === 0) {
this.notifyWarning("请选择文件!")
return
}
const data = new FormData()
this.importData.fileList.forEach((val) => {
data.append("file", val.raw, val.raw.name)
})
this.importLoading = true
$.ajax({
url: "/platform/activity/basic/scope/doImport",
type: "post",
data: data,
processData: false,
contentType: false,
success: (data) => {
this.importLoading = false
if (data.code === 0) {
this.notifySuccess("核对成功")
this.errorInfoData = data.data
this.$set(this.pageForm, "existsLoginNameRedisKey", data.data.existsLoginNameRedisKey)
} else {
this.notifyWarning("核对失败")
}
},
error: () => {
this.notifyWarning("导入失败")
this.importLoading = false
}
})
},
clearSearchCnd() {
const existsLoginNameRedisKey = this.pageForm.existsLoginNameRedisKey
this.$set(this.importData, "fileList", [])
this.errorInfoData = {
totalCount: 0,
successCount: 0,
errorCount: 0,
errorList: [],
}
this.$set(this.pageForm, "existsLoginNameRedisKey", "")
this.doSearch()
this.dialogVisible = false
$.get('/platform/activity/basic/scope/clearSearchCnd', {existsLoginNameRedisKey: existsLoginNameRedisKey}).then(res => {
if (res.code === 0) {
this.$message.success(res.msg)
} else {
this.$message.error(res.msg)
}
})
},
openImport() {
this.dialogVisible = true
},
handleImportFileRemove(file, fileList) {
this.$set(this.importData, "fileList", this.fileHandleRemove(file, fileList))
},
handleImportFileChange(file, fileList) {
this.$set(this.importData, "fileList", this.fileHandleChange(file, fileList, {type: ['xls', 'xlsx']}))
},
doExportUser() {
const {
userId,
@@ -476,7 +625,8 @@ module.exports = {
activityGroupId,
age,
reverseSelection,
activityUserCnd
activityUserCnd,
existsLoginNameRedisKey
} = this.pageForm
let props = {}
@@ -495,6 +645,7 @@ module.exports = {
"&reverseSelection=" + reverseSelection +
"&clubId=" + clubId +
"&activityGroupId=" + activityGroupId +
"&existsLoginNameRedisKey=" + existsLoginNameRedisKey +
"&teacherMeetingId=" + teacherMeetingId +
"&unionId=" + unionId +
"&unitId=" + unitId
@@ -514,6 +665,7 @@ module.exports = {
this.pageForm.clubId = ''
this.pageForm.activityGroupId = ''
this.pageForm.activityUserCnd = ''
this.pageForm.existsLoginNameRedisKey = ''
this.pageForm.age = [0, 0]
this.pageForm.reverseSelection = false
this.doSearch()
@@ -527,6 +527,7 @@ layout("/mobile/platform.html"){
minDate: new Date(2010, 0, 1),
maxDate: new Date(2010, 0, 31),
holidays: [],
fullDays: [],
timeVisible: false,
active: 0,
tarBarActive: 0,
@@ -623,6 +624,12 @@ layout("/mobile/platform.html"){
this.$refs.cal.reset(null)
break
}
if(this.fullDays.includes(afterDateStr)) {
this.$toast(afterDateStr + '报名人数已满,请更换出行时间')
this.chooseDate = []
this.$refs.cal.reset(null)
break
}
this.chooseDate.push(afterDateStr)
if(i === (this.lotValue - 1)) {
@@ -639,6 +646,10 @@ layout("/mobile/platform.html"){
if(this.timeArray.indexOf(date) === -1) {
day.type = 'disabled'
}
if(this.fullDays.includes(date)) {
day.type = 'disabled'
day.bottomInfo = '已满'
}
if(this.chooseDate.length > 0 && this.chooseDate.includes(date)) {
day.className = 'cal_middle'
if(date === this.chooseDate[this.chooseDate.length - 1]) {
@@ -801,6 +812,7 @@ layout("/mobile/platform.html"){
})
this.timeArray = res.data.days
this.holidays = res.data.holidays
this.$set(this, 'fullDays', res.data.fullDays || [])
//this.lotValue = Number(res.data.lotValue)
this.minDate = new Date(res.data.minDate)
this.maxDate = new Date(res.data.maxDate)
@@ -96,6 +96,9 @@ layout("/layouts/platform.html"){
<span v-if="row.lotId===item.id">{{item.lotName}}</span>
</div>
</template>
<template scope="{row}" v-else-if="column.prop==='maxSignCount'">
<span>{{row.maxSignCount == null || row.maxSignCount === '' || Number(row.maxSignCount) <= 0 ? '无限制' : row.maxSignCount}}</span>
</template>
<template scope="{row}" v-else-if="column.prop==='createUserName'">
{{row.createUserName + '' + row.createUnionName + ''}}
</template>
@@ -202,7 +205,7 @@ layout("/layouts/platform.html"){
</el-row>
<el-row :gutter="20">
<el-col :md="6" :sm="24" :xs="24">
<el-col :md="12" :sm="24" :xs="24">
<el-form-item label="活动范围" prop="regionalNature">
<el-radio-group size="small" v-model="formData.regionalNature">
<el-radio label="省内" border>省内</el-radio>
@@ -210,7 +213,16 @@ layout("/layouts/platform.html"){
</el-radio-group>
</el-form-item>
</el-col>
<el-col :md="6" :sm="24" :xs="24">
<el-col :md="12" :sm="24" :xs="24">
<el-form-item label="每日最高人数" prop="maxSignCount">
<el-input-number :min="0" :precision="0" placeholder="0为不限制"
style="width: 100%" v-model="formData.maxSignCount"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :md="12" :sm="24" :xs="24">
<el-form-item label="周几入住" prop="weekIn">
<el-radio-group size="small" v-model="formData.weekIn" @input="weekChange">
<el-radio :label="1" border>不限</el-radio>
@@ -508,6 +520,7 @@ layout("/layouts/platform.html"){
baseContactNumber: '',
weekIn: 1,
allowDay: [],
maxSignCount: 0,
travelAgencyPlace: '',
},
tableColumns: [
@@ -515,6 +528,7 @@ layout("/layouts/platform.html"){
{label: '目的地名称', prop: 'baseName', sortable: true},
{label: '承担旅行社', prop: 'travelAgencyName', sortable: true},
//{label: '标段时间', prop: 'lotId', sortable: true},
{label: '每日最高人数', prop: 'maxSignCount', sortable: true},
{label: '联系人', prop: 'baseContactPerson', sortable: true},
{label: '联系人电话', prop: 'baseContactNumber', sortable: true},
//{label: '活动时间', prop: 'activityTime', sortable: true},
@@ -543,6 +557,7 @@ layout("/layouts/platform.html"){
activityEndTime: [{required: true, validator: validateActivityEndTime, trigger: ['change', 'blur']}],
estimatedCost: [{required: true, message: '请输入预计费用', trigger: ['change', 'blur']}],
allowDay: [{required: true, message: '请选择天数', trigger: ['change', 'blur']}],
maxSignCount: [{required: false, message: '请输入每日最高人数', trigger: ['change', 'blur']}],
travelAgencyPlace: [{required: true, message: '请选择旅行社地点', trigger: ['change', 'blur']}],
},
//目的地导入
@@ -171,6 +171,8 @@ layout("/layouts/platform.html"){
<template #func>
<!-- <el-button icon="el-icon-s-promotion" @click="openImport" size="small" type="primary">参加人员导入-->
<!-- </el-button>-->
<el-button icon="el-icon-upload" @click="openImport" size="small" type="primary">参加人员导入
</el-button>
<el-button icon="el-icon-s-promotion" size="small" type="primary" @click="noSignExport">
导出未报名人员
</el-button>
@@ -282,6 +284,48 @@ layout("/layouts/platform.html"){
<set-up-part ref="setUpRef"></set-up-part>
<edit-form ref="editRef" @refresh="doSearch"></edit-form>
<el-dialog
title="参加人员导入"
:visible.sync="importVisible"
:close-on-click-modal="false"
width="50%">
<el-timeline>
<el-timeline-item timestamp="下载模板" placement="top">
<el-card>
<el-button size="medium" type=""
@click="downloadImportTemplate"
icon="el-icon-download">下载模板
</el-button>
</el-card>
</el-timeline-item>
<el-timeline-item timestamp="上传文件" placement="top">
<el-card>
<el-form>
<el-upload
name="file"
ref="upload"
:on-remove="handleImportFileRemove"
:on-change="handleImportFileChange"
:auto-upload="false"
:limit="1"
:file-list="importData.fileList">
<el-button size="medium" type="" icon="el-icon-upload" style="width: 200px">选择文件
</el-button>
<div class="el-upload__tip" slot="tip" style="color: #F56C6C">
只能上传 xls/xlsx 文件,时间格式请书写为2008-10-1
</div>
</el-upload>
</el-form>
</el-card>
</el-timeline-item>
</el-timeline>
<span slot="footer" class="dialog-footer">
<el-button @click="importVisibleClose" :disabled="importLoading">取 消</el-button>
<el-button type="primary" @click="doImport" :loading="importLoading">确定</el-button>
</span>
</el-dialog>
</div>
<script>
@@ -318,11 +362,17 @@ layout("/layouts/platform.html"){
{prop: 'lineOrMaName', label: '线路'},
{prop: 'times', label: '出行时间'},
{prop: 'isFamily', label: '是否携带家属'},
{prop: 'isTakePartIn', label: '是否参加'}
{prop: 'isTakePartIn', label: '是否参加', sortable: true}
],
viewVisible: false,
unionOptions: [],
config: {}
config: {},
// 参加人员导入弹窗和文件上传状态。
importVisible: false,
importLoading: false,
importData: {
fileList: []
}
}
},
methods: {
@@ -389,7 +439,52 @@ layout("/layouts/platform.html"){
},
openImport() {
this.$set(this.importData, 'fileList', [])
this.importVisible = true
},
handleImportFileRemove(file, fileList) {
this.$set(this.importData, 'fileList', this.fileHandleRemove(file, fileList))
},
handleImportFileChange(file, fileList) {
this.$set(this.importData, 'fileList', this.fileHandleChange(file, fileList, {type: ['xls', 'xlsx']}))
},
downloadImportTemplate() {
window.open('/platform/theRapyRecuperation/user/query/downloadImportTemplate')
},
doImport() {
if (this.importData.fileList.length === 0) {
this.$message.warning('请选择文件!')
return
}
const data = new FormData()
this.importData.fileList.forEach((val) => {
data.append('file', val.raw, val.raw.name)
})
this.importLoading = true
$.ajax({
url: '/platform/theRapyRecuperation/user/query/enrollImport',
type: 'post',
data: data,
processData: false,
contentType: false,
success: (resp) => {
if (resp.code === 0) {
this.importVisible = false
this.doSearch()
} else {
this.$message.warning(resp.msg)
}
this.importLoading = false
},
error: () => {
this.$message.warning('导入失败')
this.importLoading = false
}
})
},
importVisibleClose() {
this.importVisible = false
this.doSearch()
},
noSignExport() {
window.open('/platform/theRapyRecuperation/schoolUnionUserQuery/noSignExport')
@@ -27,6 +27,16 @@ layout("/layouts/platform.html"){
<h3 style="color: rgb(24, 103, 176);font-family: Microsoft YaHei;">疗休养配置</h3>
</div>
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
<el-select v-model="currentYear" style="width: 160px;" @change="changeYear">
<el-option v-for="item in yearOptions"
:key="item"
:label="item + '年度'"
:value="item"></el-option>
</el-select>
<el-button type="primary" plain @click="copyLastYear" :loading="copyLoading">沿用去年配置</el-button>
</div>
<el-form ref="addForm" :model="formData" :rules="formRules" label-width="170px">
<vi-title title="基本配置"></vi-title>
<el-form-item label="配置名称" prop="configName">
@@ -326,10 +336,18 @@ layout("/layouts/platform.html"){
const vue = new Vue({
el: '#app',
data() {
const startYear = 2025
const yearOptions = [startYear, startYear + 1, startYear + 2, startYear + 3, startYear + 4]
const nowYear = new Date().getFullYear()
const currentYear = yearOptions.includes(nowYear) ? nowYear : startYear
return {
currentYear: currentYear,
yearOptions: yearOptions,
activityGroupList: [],
subLoading: false,
copyLoading: false,
formData: {
configYear: currentYear,
isSnLine: 0,
isSwLine: 0,
lots: [],
@@ -384,6 +402,52 @@ layout("/layouts/platform.html"){
'drawer-user-scope': httpVueLoader('/components/plugins/DrawerUserScope.vue?v=1.0.1'),
},
methods: {
getDefaultFormData(configYear) {
return {
configYear: configYear,
isSnLine: 0,
isSwLine: 0,
lots: [],
configName: '智慧工会疗休养配置',
outsideQuota: null,
outsideQuotaProportion: 0,
outsideNumber: null,
travelFrequency: null,
groupNumber: null,
modifyDays: null,
modifyNumber: null,
allLineSignUpNumber: null,
notice: null,
provinceStartYear: null,
bedInfo: true,
familyInfo: 1,
}
},
async changeYear() {
await this.findOne()
},
async copyLastYear() {
if (this.formData.id) {
const confirm = await this.$confirm('当前年度已有配置,沿用去年配置会覆盖当前年度配置,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
if (confirm !== 'confirm') return
}
this.$set(this, 'copyLoading', true)
try {
const resp = await $.post(loc() + '/copyLastYear', {year: this.currentYear})
if (resp.code === 0 && resp.data) {
this.$notify.success({title: '成功', message: '已沿用去年配置'});
await this.findOne()
} else {
this.$notify.warning({title: '提示', message: resp.msg || '上一年度暂无可沿用配置'});
}
} finally {
this.$set(this, 'copyLoading', false)
}
},
async deleteLotsRow(scope) {
//this.lotDeleteList.push(scope.row.id);
//console.log(this.lotDeleteList)
@@ -424,72 +488,83 @@ layout("/layouts/platform.html"){
spinner: 'el-icon-loading',
background: COVER_LAYER_COLOR
})
const cloneData = clone(this.formData)
try {
const cloneData = clone(this.formData)
cloneData.configYear = this.currentYear
if(this.formData.createLineTimes && this.formData.createLineTimes.length > 0) {
cloneData.createLineStartTime = this.formData.createLineTimes[0]
cloneData.createLineEndTime = this.formData.createLineTimes[1]
}
if(this.formData.createLineTimes && this.formData.createLineTimes.length > 0) {
cloneData.createLineStartTime = this.formData.createLineTimes[0]
cloneData.createLineEndTime = this.formData.createLineTimes[1]
}
if(this.formData.implodeTimes && this.formData.implodeTimes.length > 0) {
cloneData.implodeStartTime = this.formData.implodeTimes[0]
cloneData.implodeEndTime = this.formData.implodeTimes[1]
}
if(this.formData.implodeTimes && this.formData.implodeTimes.length > 0) {
cloneData.implodeStartTime = this.formData.implodeTimes[0]
cloneData.implodeEndTime = this.formData.implodeTimes[1]
}
if(this.formData.fragmentTimes && this.formData.fragmentTimes.length > 0) {
cloneData.fragmentStartTime = this.formData.fragmentTimes[0]
cloneData.fragmentEndTime = this.formData.fragmentTimes[1]
}
if(this.formData.fragmentTimes && this.formData.fragmentTimes.length > 0) {
cloneData.fragmentStartTime = this.formData.fragmentTimes[0]
cloneData.fragmentEndTime = this.formData.fragmentTimes[1]
}
if(this.formData.fragmentPlayTimes && this.formData.fragmentPlayTimes.length > 0) {
cloneData.fragmentPlayStartTime = this.formData.fragmentPlayTimes[0]
cloneData.fragmentPlayEndTime = this.formData.fragmentPlayTimes[1]
}
if(this.formData.fragmentPlayTimes && this.formData.fragmentPlayTimes.length > 0) {
cloneData.fragmentPlayStartTime = this.formData.fragmentPlayTimes[0]
cloneData.fragmentPlayEndTime = this.formData.fragmentPlayTimes[1]
}
if (this.formData.files && this.formData.files.length > 0) {
cloneData.files = JSON.stringify(this.formData.files)
}
if (this.lotDeleteList && this.lotDeleteList.length > 0) {
cloneData.lotDeleteList = JSON.stringify(this.lotDeleteList)
}
if (this.formData.lots && this.formData.lots.length > 0) {
cloneData.lots = JSON.stringify(this.formData.lots)
}
cloneData.outsideQuotaProportion = cloneData.outsideQuotaProportion / 100
const resp = await $.post(loc() + "/operation", cloneData)
if (resp.code === 0){
this.$notify.success({title: '成功', message: resp.msg});
delete cloneData.createLineTimes
delete cloneData.implodeTimes
delete cloneData.fragmentTimes
delete cloneData.fragmentPlayTimes
if (this.formData.files && this.formData.files.length > 0) {
cloneData.files = JSON.stringify(this.formData.files)
}
if (this.lotDeleteList && this.lotDeleteList.length > 0) {
cloneData.lotDeleteList = JSON.stringify(this.lotDeleteList)
}
if (this.formData.lots && this.formData.lots.length > 0) {
cloneData.lots = JSON.stringify(this.formData.lots)
}
cloneData.outsideQuotaProportion = cloneData.outsideQuotaProportion / 100
const resp = await $.post(loc() + "/operation", cloneData)
if (resp.code === 0){
this.$notify.success({title: '成功', message: resp.msg});
await this.findOne()
}else {
this.$notify.error({title: '失败', message: resp.msg});
}
} finally {
loading.close()
this.subLoading = false
}else {
this.$notify.error({title: '失败', message: resp.msg});
}
}
},
async findOne() {
await this.getActivityGroup()
const {data} = await $.get(loc() + "/findOne")
if (data.outsideQuotaProportion) {
data.outsideQuotaProportion = data.outsideQuotaProportion * 1000 / 10
const {data} = await $.get(loc() + "/findOne", {year: this.currentYear})
const formData = data || this.getDefaultFormData(this.currentYear)
this.$set(formData, 'configYear', this.currentYear)
if (formData.outsideQuotaProportion) {
this.$set(formData, 'outsideQuotaProportion', formData.outsideQuotaProportion * 1000 / 10)
}
if(data.createLineStartTime && data.createLineEndTime) {
data.createLineTimes = [data.createLineStartTime, data.createLineEndTime]
if(formData.createLineStartTime && formData.createLineEndTime) {
this.$set(formData, 'createLineTimes', [formData.createLineStartTime, formData.createLineEndTime])
}
if(data.implodeStartTime && data.implodeEndTime) {
data.implodeTimes = [data.implodeStartTime, data.implodeEndTime]
if(formData.implodeStartTime && formData.implodeEndTime) {
this.$set(formData, 'implodeTimes', [formData.implodeStartTime, formData.implodeEndTime])
}
if(data.fragmentStartTime && data.fragmentEndTime) {
data.fragmentTimes = [data.fragmentStartTime, data.fragmentEndTime]
if(formData.fragmentStartTime && formData.fragmentEndTime) {
this.$set(formData, 'fragmentTimes', [formData.fragmentStartTime, formData.fragmentEndTime])
}
if(data.fragmentPlayStartTime && data.fragmentPlayEndTime) {
data.fragmentPlayTimes = [data.fragmentPlayStartTime, data.fragmentPlayEndTime]
if(formData.fragmentPlayStartTime && formData.fragmentPlayEndTime) {
this.$set(formData, 'fragmentPlayTimes', [formData.fragmentPlayStartTime, formData.fragmentPlayEndTime])
}
if (!data.lots) {
data.lots = []
if (!formData.lots) {
this.$set(formData, 'lots', [])
}
this.formData = data
this.$set(this, 'formData', formData)
},
async getActivityGroup() {
const {data} = await $.get('/platform/activity/basic/scope/getActivityUserScopeGroup')