This commit is contained in:
2026-06-08 08:56:48 +08:00
parent 67a46212e2
commit 5cf398f2b1
53 changed files with 5613 additions and 94 deletions
@@ -193,13 +193,23 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
// 判断财务信息有没有值,
if (financeUserMap.containsKey(sysUser.getLoginname())) {
NutMap nutMap = financeUserMap.get(sysUser.getLoginname());
sysUser.setMember(true);
sysUser.setWelfareMember(true);
sysUser.setPreparationMemberFee(BigDecimal.valueOf(nutMap.getDouble("preparationMemberFee")));
sysUser.setContractMemberFee(BigDecimal.valueOf(nutMap.getDouble("contractMemberFee")));
if(BigDecimal.valueOf(nutMap.getDouble("preparationMemberFee")).doubleValue()>0
|| BigDecimal.valueOf(nutMap.getDouble("contractMemberFee")).doubleValue()>0){
sysUser.setMember(true);
sysUser.setWelfareMember(true);
sysUser.setPreparationMemberFee(BigDecimal.valueOf(nutMap.getDouble("preparationMemberFee")));
sysUser.setContractMemberFee(BigDecimal.valueOf(nutMap.getDouble("contractMemberFee")));
} else {
sysUser.setMember(false);
sysUser.setWelfareMember(false);
sysUser.setPreparationMemberFee(new BigDecimal(0));
sysUser.setContractMemberFee(new BigDecimal(0));
}
} else {
sysUser.setMember(false);
sysUser.setWelfareMember(false);
sysUser.setPreparationMemberFee(new BigDecimal(0));
sysUser.setContractMemberFee(new BigDecimal(0));
}
return sysUser;
@@ -127,7 +127,7 @@ public class ActivityCultureInfoManageController {
@At
@Ok("json:full")
@ApiOperation("通知报名人员")
@SaCheckPermission("trainSignUp.manage")
@SaCheckPermission("infoManage.school.manage")
public Result sendNotify(@Param("activityId") String activityId, @Param("content") String content) {
if (StrUtil.isBlank(activityId) || StrUtil.isBlank(content)) {
return Result.error("参数不完整");
@@ -0,0 +1,176 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.Collections;
import java.util.List;
@IocBean
@Ok("json:full")
@At("/platform/tour/branchUserAssignment")
public class TourBranchUserAssignmentController {
@Inject
private TourUserAssignmentService tourUserAssignmentService;
/**
* 分工会人员分配列表入口。
*/
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/branchUserAssignment/index.html")
@SaCheckPermission("tour.branchUserAssignment")
public void index() {
}
/**
* 分页查询当前登录人所在分工会的分配记录,列表数据只来自人员分配表。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result pageData(PageForm pageForm, Integer year, String settingId, String matterId,
String personType, String keyword) {
return Result.success(tourUserAssignmentService.branchAssignmentPage(pageForm, year, settingId, matterId,
personType, keyword));
}
/**
* 分页查询候选人员,候选范围由疗休养配置可参加人员范围和当前登录人所在分工会共同决定。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result candidatePageData(PageForm pageForm, String settingId, String keyword) {
return Result.success(tourUserAssignmentService.branchCandidatePage(pageForm, settingId, keyword));
}
/**
* 查询可用于分配的疗休养配置。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result settingOptions(Integer year) {
return Result.success(tourUserAssignmentService.listEnabledSettingOptions(year));
}
/**
* 查询分工会可分配线路选项,选项携带事项、线路和旅行社快照信息。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result matterOptions(String settingId) {
return Result.success(tourUserAssignmentService.listMatterOptions(settingId));
}
/**
* 查询当前登录人所在分工会在指定配置下的名额使用情况。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result quotaInfo(String settingId) {
return Result.success(tourUserAssignmentService.branchQuotaInfo(settingId));
}
/**
* 保存分工会人员分配,保存时数据来源固定为 BRANCH_UNION。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.branchUserAssignment")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "保存分工会人员分配")
public Result doAssign(String settingId, String matterId, String personType, @Param("userIds") String userIds) {
List<String> parsedUserIds;
try {
parsedUserIds = parseUserIds(userIds);
} catch (Exception e) {
return Result.error("人员参数错误");
}
try {
return Result.success(tourUserAssignmentService.assignBranchUsers(settingId, matterId, personType, parsedUserIds));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 切换当前分工会人员分配记录的正式/替补状态,具体名额和台账保护规则由 service 统一处理。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.branchUserAssignment")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "切换分工会人员类型")
public Result switchPersonType(String id, String personType) {
try {
return Result.success(tourUserAssignmentService.switchCurrentBranchPersonType(id, personType));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 恢复当前分工会已取消退出的人员分配记录,只恢复状态,不恢复已删除台账。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.branchUserAssignment.cancelRestore")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "取消退出分工会人员分配")
public Result restoreCancel(String id) {
try {
tourUserAssignmentService.restoreCurrentBranchCancelledAssignment(id);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 删除前检查该分配记录是否已经存在对应报名台账。
*/
@At
@SaCheckPermission("tour.branchUserAssignment")
public Result deleteInfo(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
return Result.success(tourUserAssignmentService.branchDeleteInfo(id));
}
/**
* 删除分工会人员分配记录,只删除 BRANCH_UNION 来源的数据。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.branchUserAssignment")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "删除分工会人员分配")
public Result doDelete(String id, Boolean deleteLedger) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
try {
tourUserAssignmentService.deleteCurrentBranchAssignment(id, Boolean.TRUE.equals(deleteLedger));
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
private List<String> parseUserIds(String userIds) {
if (StrUtil.isBlank(userIds)) {
return Collections.emptyList();
}
List<String> list = Json.fromJsonAsList(String.class, userIds);
return Lang.isEmpty(list) ? Collections.emptyList() : list;
}
}
@@ -213,6 +213,7 @@ public class TourGroupController {
List<NutMap> list = listSql.getList(NutMap.class);
fillSignupMobile(list);
fillSignupFamilies(list);
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
@@ -362,6 +363,50 @@ public class TourGroupController {
}
}
/**
* 给报名人员列表挂载家属子列表;当前家属台账未保存手机号,mobile 字段先返回空值供前端占位。
*/
private void fillSignupFamilies(List<NutMap> list) {
if (list == null || list.isEmpty()) {
return;
}
List<String> ledgerIds = new ArrayList<>();
for (NutMap item : list) {
String ledgerId = item.getString("id", "");
if (StrUtil.isNotBlank(ledgerId)) {
ledgerIds.add(ledgerId);
}
}
if (ledgerIds.isEmpty()) {
return;
}
Sql sql = Sqls.create("""
SELECT
ledgerId,
familyName,
age,
gender,
'' AS mobile,
idCard,
relationship
FROM tour_ledger_family
WHERE delFlag = 0
AND ledgerId IN (@ledgerIds)
ORDER BY createdAt ASC
""");
sql.setParam("ledgerIds", ledgerIds.toArray(new String[0]));
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
Map<String, List<NutMap>> familyMap = new HashMap<>();
for (NutMap family : sql.getList(NutMap.class)) {
familyMap.computeIfAbsent(family.getString("ledgerId", ""), key -> new ArrayList<>()).add(family);
}
for (NutMap item : list) {
item.put("families", familyMap.getOrDefault(item.getString("id", ""), List.of()));
}
}
private Workbook buildParticipantsWorkbook(NutMap matter, List<NutMap> list) {
Workbook workbook = new HSSFWorkbook();
Sheet sheet = workbook.createSheet("参加人员名单");
@@ -0,0 +1,78 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLeaveApplyService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
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;
@IocBean
@Ok("json:full")
@At("/platform/tour/leaveApply")
public class TourLeaveApplyController {
@Inject
private TourLeaveApplyService leaveApplyService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/leaveApply/index.html")
@SaCheckPermission("tour.leaveApply")
public void index() {
}
/**
* 查询人员分配表中的退出取消数据,旧退出申请表不再作为业务来源。
*/
@At
@SaCheckPermission("tour.leaveApply")
public Result pageData(PageForm pageForm, String keyword, String unionName, String status) {
return Result.success(leaveApplyService.pageData(pageForm, keyword, unionName, status));
}
/**
* 查询当前登录人在取消管理页的可操作权限,供前端控制按钮显示。
*/
@At
@SaCheckPermission("tour.leaveApply")
public Result permissionInfo() {
return Result.success(leaveApplyService.permissionInfo());
}
/**
* 取消人员分配记录:标记已退出,并同步删除该人员对应路线的报名台账。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.leaveApply")
@SLog(type = "tour", tag = "疗休养退出取消", msg = "取消疗休养人员分配")
public Result doCancel(String id) {
try {
leaveApplyService.cancelAssignment(id);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 恢复人员分配记录退出状态,只恢复人员分配表状态,不恢复已删除台账。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.branchUserAssignment.cancelRestore")
@SLog(type = "tour", tag = "疗休养退出取消", msg = "恢复疗休养人员分配退出状态")
public Result doRestore(String id) {
try {
leaveApplyService.restoreAssignment(id);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
}
@@ -124,6 +124,10 @@ public class TourLedgerController {
IF(IFNULL(l.directFamilyUnitLine, 0) = 1 OR dr.id IS NOT NULL, 1, 0) AS directFamilyUnitLine,
dr.id AS directRelativeId,
COALESCE(NULLIF(l.lineName, ''), t.lineName) AS currentLineName,
COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS currentGender,
vu.age AS age,
COALESCE(NULLIF(vu.idCard, ''), t.idCard, '') AS currentIdCard,
COALESCE(NULLIF(vu.mobile, ''), '') AS mobile,
IFNULL(f.familyCount, 0) AS familyCount,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
@@ -141,6 +145,7 @@ public class TourLedgerController {
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
LEFT JOIN vw_user vu ON vu.loginname = t.jobNo
LEFT JOIN (
SELECT
`year`,
@@ -172,7 +177,11 @@ public class TourLedgerController {
tourLedgerService.dao().execute(listSql);
var list = listSql.getList(NutMap.class);
list.forEach(item -> item.put("lineName", item.getString("currentLineName")));
list.forEach(item -> {
item.put("lineName", item.getString("currentLineName"));
item.put("gender", item.getString("currentGender"));
item.put("idCard", item.getString("currentIdCard"));
});
Pagination<NutMap> pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list);
return Result.success(pagination);
}
@@ -313,6 +322,114 @@ public class TourLedgerController {
CommonDownloadUtil.download("参加人员导入模板.xlsx", workbook, response);
}
/**
* 按当前查询条件导出台账列表,导出字段与页面列表显示字样保持一致。
*/
@At
@SaCheckPermission("tour.ledger")
@Ok("void")
public void exportLedgerData(Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly,
HttpServletResponse response) {
List<ExcelExportEntity> entities = new ArrayList<>();
entities.add(new ExcelExportEntity("工号", "jobNo", 16));
entities.add(new ExcelExportEntity("姓名", "userName", 14));
entities.add(new ExcelExportEntity("性别", "gender", 10));
entities.add(new ExcelExportEntity("年龄", "age", 10));
entities.add(new ExcelExportEntity("身份证号", "idCard", 24));
entities.add(new ExcelExportEntity("手机号", "mobile", 18));
entities.add(new ExcelExportEntity("所在单位", "unitName", 26));
entities.add(new ExcelExportEntity("乘车地点", "boardingPlace", 18));
entities.add(new ExcelExportEntity("报名线路", "lineName", 30));
entities.add(new ExcelExportEntity("出行时段", "travelPeriod", 30));
entities.add(new ExcelExportEntity("线路类型", "lineType", 16));
entities.add(new ExcelExportEntity("报名时间", "signupTime", 22));
entities.add(new ExcelExportEntity("携带家属", "familyCountText", 14));
entities.add(new ExcelExportEntity("是否参加", "joinedText", 14));
entities.add(new ExcelExportEntity("报销超出费用", "overCostReimbursedText", 18));
List<NutMap> rows = queryLedgerExportRows(startYear, endYear, keyword, unionId, lineId, travelPeriod,
lineType, directFamilyOnly, overCostOnly);
rows.forEach(row -> {
Integer familyCount = row.getInt("familyCount");
row.put("familyCountText", (familyCount == null ? 0 : familyCount) + "");
row.put("joinedText", Boolean.TRUE.equals(row.getBoolean("joined")) ? "" : "");
row.put("overCostReimbursedText", Boolean.TRUE.equals(row.getBoolean("overCostReimbursed")) ? "" : "");
});
ExportParams exportParams = new ExportParams();
exportParams.setSheetName("疗休养台账");
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, rows);
CommonDownloadUtil.download("疗休养台账数据.xlsx", workbook, response);
}
private List<NutMap> queryLedgerExportRows(Integer startYear, Integer endYear, String keyword, String unionId, String lineId,
String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) {
Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly);
Sql sql = Sqls.create("""
SELECT
t.id,
t.jobNo,
t.userName,
COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS gender,
vu.age AS age,
COALESCE(NULLIF(vu.idCard, ''), t.idCard, '') AS idCard,
COALESCE(NULLIF(vu.mobile, ''), '') AS mobile,
t.unitName,
t.boardingPlace,
COALESCE(NULLIF(l.lineName, ''), t.lineName, '') AS lineName,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE tp.travelPeriod
END AS travelPeriod,
t.lineType,
t.signupTime,
IFNULL(f.familyCount, 0) AS familyCount,
t.joined,
t.overCostReimbursed
FROM tour_ledger t
LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
WHERE delFlag = 0
GROUP BY ledgerId
) f ON f.ledgerId = t.id
LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0
LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0
LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0
LEFT JOIN vw_user vu ON vu.loginname = t.jobNo
LEFT JOIN (
SELECT
`year`,
lineId,
GROUP_CONCAT(
DISTINCT CASE
WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> ''
THEN CONCAT(travelStartTime, ' 至 ', travelEndTime)
ELSE NULL
END
ORDER BY travelStartTime ASC
SEPARATOR ''
) AS travelPeriod
FROM tour_matter
WHERE delFlag = 0
AND lineId IS NOT NULL
AND lineId <> ''
GROUP BY `year`, lineId
) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId
$condition
GROUP BY t.id
ORDER BY t.signupTime DESC, t.createdAt DESC
""");
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourLedgerService.dao().execute(sql);
return sql.getList(NutMap.class);
}
@At
@SaCheckPermission("tour.ledger")
@Ok("void")
@@ -263,6 +263,19 @@ public class TourMatterController {
.addv("maxGroupPeople", setting.getMaxGroupPeople()));
}
@At
@SaCheckPermission("tour.matter")
public Result settingBoardingPlaces(String settingId) {
if (StrUtil.isBlank(settingId)) {
return Result.error("参数错误");
}
TourSetting setting = tourMatterService.dao().fetch(TourSetting.class, settingId);
if (setting == null) {
return Result.error("疗休养配置不存在");
}
return Result.success(NutMap.NEW().addv("boardingPlace", StrUtil.blankToDefault(setting.getBoardingPlace(), "[]")));
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.matter")
@@ -277,6 +290,8 @@ public class TourMatterController {
return Result.error("事项不存在");
}
oldMatter.setLineId(matter.getLineId());
// 默认乘车地点来自疗休养配置中的乘车地点列表,后续代报名和自主报名会优先带出该值。
oldMatter.setDefaultBoardingPlace(matter.getDefaultBoardingPlace());
oldMatter.setSignupStartTime(matter.getSignupStartTime());
oldMatter.setSignupEndTime(matter.getSignupEndTime());
oldMatter.setTravelStartTime(matter.getTravelStartTime());
@@ -31,6 +31,7 @@ import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeSer
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
@@ -84,6 +85,9 @@ public class TourMySignupController {
@Inject
private TourMatterService tourMatterService;
@Inject
private TourUserAssignmentService tourUserAssignmentService;
@Inject
private SysDictService sysDictService;
@@ -284,6 +288,7 @@ public class TourMySignupController {
m.`year`,
m.matterName,
m.unionId AS matterUnionId,
m.defaultBoardingPlace,
m.travelStartTime,
m.travelEndTime,
CASE
@@ -299,7 +304,8 @@ public class TourMySignupController {
a.agencyName AS travelAgencyName,
IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement,
s.allowFamily,
IFNULL(s.fillBedInfo, 1) AS fillBedInfo
IFNULL(s.fillBedInfo, 1) AS fillBedInfo,
s.boardingPlace AS boardingPlaceOptions
FROM tour_matter m
INNER JOIN tour_line l ON l.id = m.lineId
LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId
@@ -408,6 +414,15 @@ public class TourMySignupController {
return Result.success(sql.getList(Sys_dict.class));
}
/**
* 我的报名修改接口,PC 和 H5 共用。
* 修改成功后更新疗休养台账,并回填当前用户已存在的人员分配记录事项快照。
*
* @param ledger 报名台账主信息
* @param families 家属信息 JSON 数组
* @param directRelative 直系亲属线路报名信息 JSON
* @return 修改结果;直系亲属线路、超额报销等场景会返回待审核提示
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR)
@@ -440,6 +455,10 @@ public class TourMySignupController {
TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId());
boolean directFamilyLine = line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine());
TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId());
Result boardingPlaceResult = normalizeBoardingPlace(ledger, matter, setting);
if (boardingPlaceResult != null) {
return boardingPlaceResult;
}
if (setting != null && Boolean.FALSE.equals(setting.getFillBedInfo())) {
clearBedInfo(ledger, familyList);
}
@@ -468,6 +487,8 @@ public class TourMySignupController {
fillStaffInfo(ledger);
tourLedgerService.updateIgnoreNull(ledger);
// 报名台账更新后,仅回填已存在的人员分配记录事项信息,不新增记录、不改变分配来源。
tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), ledger.getBoardingPlace());
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
if (Lang.isNotEmpty(familyList)) {
@@ -511,6 +532,7 @@ public class TourMySignupController {
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
clearAssignmentMatterAfterCancel(ledger);
return Result.success().addMsg("取消报名成功");
}
@@ -526,9 +548,21 @@ public class TourMySignupController {
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
clearAssignmentMatterAfterCancel(ledger);
return Result.success().addMsg("取消报名成功");
}
private void clearAssignmentMatterAfterCancel(TourLedger ledger) {
if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) {
return;
}
TourMatter matter = tourMatterService.fetch(ledger.getMatterId());
if (matter == null || StrUtil.isBlank(matter.getSettingId())) {
return;
}
tourUserAssignmentService.clearExistingAssignmentMatterAfterCancel(matter.getSettingId(), SecurityUtil.getUserId());
}
@At
@SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR)
public Result lineOptions(Integer startYear, Integer endYear) {
@@ -590,6 +624,60 @@ public class TourMySignupController {
return null;
}
/**
* 修改报名时重新校验乘车地点,确保提交值仍来自当前疗休养配置的乘车地点列表。
*/
private Result normalizeBoardingPlace(TourLedger ledger, TourMatter matter, TourSetting setting) {
if (ledger == null || setting == null) {
return null;
}
List<String> options = parseBoardingPlaceOptions(setting.getBoardingPlace());
if (Lang.isEmpty(options)) {
ledger.setBoardingPlace("");
return null;
}
String boardingPlace = StrUtil.blankToDefault(ledger.getBoardingPlace(), matter == null ? "" : matter.getDefaultBoardingPlace());
if (StrUtil.isBlank(boardingPlace)) {
return Result.error("请选择乘车地点");
}
if (!options.contains(boardingPlace)) {
return Result.error("乘车地点不在当前疗休养配置范围内");
}
ledger.setBoardingPlace(boardingPlace);
return null;
}
private List<String> parseBoardingPlaceOptions(String value) {
if (StrUtil.isBlank(value)) {
return Collections.emptyList();
}
try {
List<?> list = Json.fromJson(List.class, value);
if (Lang.isEmpty(list)) {
return Collections.emptyList();
}
return list.stream().map(item -> {
if (item instanceof String) {
return ((String) item).trim();
}
if (item instanceof java.util.Map) {
Object name = ((java.util.Map<?, ?>) item).get("name");
return name == null ? "" : String.valueOf(name).trim();
}
return "";
})
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
} catch (Exception e) {
return java.util.Arrays.stream(value.split(","))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
}
}
private Result checkSignupRule(TourLedger ledger, TourMatter matter, TourLedger oldLedger) {
if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) {
return Result.error("报名出行时段不存在或已停用");
@@ -0,0 +1,207 @@
package com.budwk.app.zhgh.dayofficework.tour.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
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.Ok;
import org.nutz.mvc.annotation.Param;
import java.util.Collections;
import java.util.List;
@IocBean
@Ok("json:full")
@At("/platform/tour/schoolUserAssignment")
public class TourSchoolUserAssignmentController {
@Inject
private TourUserAssignmentService tourUserAssignmentService;
/**
* 校工会人员分配列表入口。
*/
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/schoolUserAssignment/index.html")
@SaCheckPermission("tour.schoolUserAssignment")
public void index() {
}
/**
* 分页查询校工会分配记录,列表数据只来自人员分配表。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result pageData(PageForm pageForm, Integer year, String settingId, String matterId, String unionId,
String personType, String keyword) {
return Result.success(tourUserAssignmentService.schoolAssignmentPage(pageForm, year, settingId, matterId,
unionId, personType, keyword));
}
/**
* 分页查询候选人员,候选范围由疗休养配置的可参加人员范围决定。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result candidatePageData(PageForm pageForm, String settingId, String unionId, String keyword) {
return Result.success(tourUserAssignmentService.schoolCandidatePage(pageForm, settingId, unionId, keyword));
}
/**
* 查询可用于分配的疗休养配置。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result settingOptions(Integer year) {
return Result.success(tourUserAssignmentService.listEnabledSettingOptions(year));
}
/**
* 查询校工会可分配线路选项,选项携带事项、线路和旅行社快照信息。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result matterOptions(String settingId) {
return Result.success(tourUserAssignmentService.listMatterOptions(settingId));
}
/**
* 查询分工会选项,供筛选和候选人员过滤使用。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result unionOptions() {
return Result.success(tourUserAssignmentService.listUnionOptions());
}
/**
* 保存校工会人员分配,保存时数据来源固定为 SCHOOL_UNION。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.schoolUserAssignment")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "保存校工会人员分配")
public Result doAssign(String settingId, String matterId, String personType, @Param("userIds") String userIds) {
List<String> parsedUserIds;
try {
parsedUserIds = parseUserIds(userIds);
} catch (Exception e) {
return Result.error("人员参数错误");
}
try {
return Result.success(tourUserAssignmentService.assignSchoolUsers(settingId, matterId, personType, parsedUserIds));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 保存校工会人员分配明细,支持弹窗候选列表中每个人单独选择分配线路。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.schoolUserAssignment")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "保存校工会人员分配明细")
public Result doAssignItems(String settingId, @Param("assignItems") String assignItems) {
List<NutMap> parsedItems;
try {
parsedItems = parseAssignItems(assignItems);
} catch (Exception e) {
return Result.error("人员分配明细参数错误");
}
try {
return Result.success(tourUserAssignmentService.assignSchoolUsers(settingId, parsedItems));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 给校工会已分配人员补选分配线路,具体回写分配表和写台账逻辑由 service 统一处理。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.schoolUserAssignment")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "选择校工会分配事项")
public Result selectMatter(String id, String matterId) {
try {
return Result.success(tourUserAssignmentService.selectSchoolAssignmentMatter(id, matterId));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 恢复校工会已取消退出的人员分配记录,只恢复状态,不恢复已删除台账。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.schoolUserAssignment.cancelRestore")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "取消退出校工会人员分配")
public Result restoreCancel(String id) {
try {
tourUserAssignmentService.restoreCancelledAssignmentBySource(id, TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 删除前检查该分配记录是否已经存在对应报名台账。
*/
@At
@SaCheckPermission("tour.schoolUserAssignment")
public Result deleteInfo(String id) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
return Result.success(tourUserAssignmentService.schoolDeleteInfo(id));
}
/**
* 删除校工会人员分配记录,只删除 SCHOOL_UNION 来源的数据。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.schoolUserAssignment")
@SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "删除校工会人员分配")
public Result doDelete(String id, Boolean deleteLedger) {
if (StrUtil.isBlank(id)) {
return Result.error("参数错误");
}
try {
tourUserAssignmentService.deleteBySource(id, TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION, Boolean.TRUE.equals(deleteLedger));
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
private List<String> parseUserIds(String userIds) {
if (StrUtil.isBlank(userIds)) {
return Collections.emptyList();
}
List<String> list = Json.fromJsonAsList(String.class, userIds);
return Lang.isEmpty(list) ? Collections.emptyList() : list;
}
private List<NutMap> parseAssignItems(String assignItems) {
if (StrUtil.isBlank(assignItems)) {
return Collections.emptyList();
}
List<NutMap> list = Json.fromJsonAsList(NutMap.class, assignItems);
return Lang.isEmpty(list) ? Collections.emptyList() : list;
}
}
@@ -9,6 +9,7 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingLot;
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.ioc.aop.Aop;
@@ -16,6 +17,7 @@ import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
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.Ok;
import org.nutz.mvc.annotation.Param;
@@ -29,9 +31,15 @@ import java.util.stream.Collectors;
@At("/platform/tour/setting")
public class TourSettingController {
private static final String SIGNUP_ELIGIBILITY_MODE_SCOPE_GROUP = "SCOPE_GROUP";
private static final String SIGNUP_ELIGIBILITY_MODE_ASSIGNED_USER = "ASSIGNED_USER";
@Inject
private TourSettingService tourSettingService;
@Inject
private TourUserAssignmentService tourUserAssignmentService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/setting/index.html")
@SaCheckPermission("tour.setting")
@@ -69,6 +77,7 @@ public class TourSettingController {
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(TourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(id));
return Result.success(tourSetting);
}
@@ -89,15 +98,31 @@ public class TourSettingController {
Cnd lotCnd = Cnd.NEW();
lotCnd.desc(TourSettingLot::getLotValue);
tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd);
tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(tourSetting.getId()));
return Result.success(tourSetting);
}
@At
@SaCheckPermission("tour.setting")
public Result unionQuotaRows(String settingId) {
// 新增配置时 settingId 为空,service 会返回所有分工会的空名额行;编辑时合并已保存名额。
return Result.success(tourSettingService.listUnionQuotaRows(settingId));
}
@At
@SaCheckPermission("tour.setting")
public Result unionQuotaOverview(String settingId) {
int schoolFormalAssignedCount = tourUserAssignmentService.countSchoolFormalAssignedUsers(settingId);
return Result.success(NutMap.NEW().addv("schoolFormalAssignedCount", schoolFormalAssignedCount));
}
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.setting")
@SLog(type = "tour", tag = "疗休养设置", msg = "保存疗休养配置")
public Result doSubmit(TourSetting tourSetting,
@Param(value = "lots") String lots,
@Param(value = "unionQuotas") String unionQuotas,
@Param(value = "lotDeleteList") String[] lotDeleteList) {
Result checkResult = check(tourSetting);
if (checkResult != null) {
@@ -114,6 +139,13 @@ public class TourSettingController {
}
// 布尔值给默认值,避免前端未传时出现空状态。
int schoolFormalAssignedCount = tourUserAssignmentService.countSchoolFormalAssignedUsers(tourSetting.getId());
int branchTotalQuota = Math.max(defaultInt(tourSetting.getTravelPeopleQuota()) - schoolFormalAssignedCount, 0);
String quotaLimitMessage = tourSettingService.checkBranchFormalQuotaLimit(unionQuotas, branchTotalQuota);
if (StrUtil.isNotBlank(quotaLimitMessage)) {
return Result.error(quotaLimitMessage);
}
if (tourSetting.getEnabled() == null) {
tourSetting.setEnabled(true);
}
@@ -123,6 +155,10 @@ public class TourSettingController {
if (tourSetting.getFillBedInfo() == null) {
tourSetting.setFillBedInfo(true);
}
// 报名资格校验方式默认按人员分配表校验,后续报名校验切换会读取该配置。
if (StrUtil.isBlank(tourSetting.getSignupEligibilityMode())) {
tourSetting.setSignupEligibilityMode(SIGNUP_ELIGIBILITY_MODE_ASSIGNED_USER);
}
if (StrUtil.isBlank(tourSetting.getOutProvinceRatioType())) {
tourSetting.setOutProvinceRatioType("当年报名人数");
}
@@ -148,6 +184,7 @@ public class TourSettingController {
} else {
tourSettingService.insertWith(tourSetting, "lots");
}
tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas);
} else {
// 编辑时先处理页面删除的标段,再保存配置和当前标段行。
if (Lang.isNotEmpty(lotDeleteList)) {
@@ -155,6 +192,7 @@ public class TourSettingController {
}
tourSettingService.updateIgnoreNull(tourSetting);
saveLots(tourSetting);
tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas);
}
return Result.success();
}
@@ -168,6 +206,8 @@ public class TourSettingController {
return Result.error("参数错误");
}
tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getSettingId, "=", id));
tourSettingService.clearUnionQuotas(id);
tourUserAssignmentService.clearBySettingId(id);
tourSettingService.delete(id);
return Result.success();
}
@@ -205,6 +245,18 @@ public class TourSettingController {
if (StrUtil.isBlank(tourSetting.getConfigName())) {
return Result.error("配置名称不能为空");
}
Result boardingPlaceCheck = normalizeBoardingPlace(tourSetting);
if (boardingPlaceCheck != null) {
return boardingPlaceCheck;
}
if (tourSetting.getTravelPeopleQuota() != null && tourSetting.getTravelPeopleQuota() < 0) {
return Result.error("出行人数指标不能小于0");
}
if (StrUtil.isNotBlank(tourSetting.getSignupEligibilityMode())
&& !SIGNUP_ELIGIBILITY_MODE_SCOPE_GROUP.equals(tourSetting.getSignupEligibilityMode())
&& !SIGNUP_ELIGIBILITY_MODE_ASSIGNED_USER.equals(tourSetting.getSignupEligibilityMode())) {
return Result.error("报名资格校验方式不正确");
}
if (tourSetting.getMinGroupPeople() != null && tourSetting.getMaxGroupPeople() != null
&& tourSetting.getMinGroupPeople() > tourSetting.getMaxGroupPeople()) {
return Result.error("最少成团人数不能大于最多成团人数");
@@ -227,6 +279,33 @@ public class TourSettingController {
return null;
}
private int defaultInt(Integer value) {
return value == null || value < 0 ? 0 : value;
}
private Result normalizeBoardingPlace(TourSetting tourSetting) {
if (StrUtil.isBlank(tourSetting.getBoardingPlace())) {
tourSetting.setBoardingPlace("[]");
return null;
}
try {
List<NutMap> rows = Json.fromJsonAsList(NutMap.class, tourSetting.getBoardingPlace());
if (Lang.isEmpty(rows)) {
tourSetting.setBoardingPlace("[]");
return null;
}
// 乘车路线以 JSON 数组保存,只保留有效路线名称,避免页面空行写入配置。
List<NutMap> normalizedRows = rows.stream()
.filter(item -> item != null && StrUtil.isNotBlank(item.getString("name")))
.map(item -> NutMap.NEW().addv("name", item.getString("name").trim()))
.collect(Collectors.toList());
tourSetting.setBoardingPlace(Json.toJson(normalizedRows));
return null;
} catch (Exception e) {
return Result.error("乘车路线格式不正确");
}
}
private Result checkLots(List<TourSettingLot> lots) {
if (Lang.isEmpty(lots)) {
return null;
@@ -30,6 +30,7 @@ import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeSer
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
@@ -63,6 +64,9 @@ public class TourSignupController {
@Inject
private TourMatterService tourMatterService;
@Inject
private TourUserAssignmentService tourUserAssignmentService;
@Inject
private TourLedgerService tourLedgerService;
@@ -405,6 +409,7 @@ public class TourSignupController {
m.`year`,
m.matterName,
m.unionId AS matterUnionId,
m.defaultBoardingPlace,
m.travelStartTime,
m.travelEndTime,
m.contactName,
@@ -421,7 +426,8 @@ public class TourSignupController {
a.agencyName AS travelAgencyName,
IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement,
s.allowFamily,
IFNULL(s.fillBedInfo, 1) AS fillBedInfo
IFNULL(s.fillBedInfo, 1) AS fillBedInfo,
s.boardingPlace AS boardingPlaceOptions
FROM tour_matter m
INNER JOIN tour_line l ON l.id = m.lineId
LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId
@@ -484,6 +490,13 @@ public class TourSignupController {
.addv("directRelative", directRelative));
}
/**
* 报名前校验接口,PC 和 H5 共用。
* 用于在进入报名表单前校验事项、线路、配置状态、人员范围、重复报名和线路报名规则。
*
* @param matterId 疗休养事项ID
* @return 可报名返回成功;不可报名返回对应业务提示
*/
@At
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result signupEligibilityNotice(String matterId) {
@@ -502,9 +515,13 @@ public class TourSignupController {
if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) {
return Result.error("报名事项配置不存在或已停用");
}
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
return Result.error("您不在本次疗休养报名范围内");
Result timeResult = checkSignupTime(matter);
if (timeResult != null) {
return timeResult;
}
Result scopeResult = checkSignupEligibilityScope(setting);
if (scopeResult != null) {
return scopeResult;
}
TourLedger oldLedger = fetchCurrentUserMatterLedger(matter.getId());
CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger);
@@ -532,7 +549,7 @@ public class TourSignupController {
.addv("currentLineCost", cycleTotalCost.currentCost())
.addv("message", buildCycleTotalCostNotice(cycleTotalCost)));
}
if (!isOutProvinceLine(line.getLineType())) {
if (isAssignedUserEligibility(setting) || !isOutProvinceLine(line.getLineType())) {
return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false));
}
OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger);
@@ -570,9 +587,13 @@ public class TourSignupController {
if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) {
return Result.error("报名事项配置不存在或已停用");
}
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
return Result.error("您不在本次疗休养报名范围内");
Result timeResult = checkSignupTime(matter);
if (timeResult != null) {
return timeResult;
}
Result scopeResult = checkSignupEligibilityScope(setting);
if (scopeResult != null) {
return scopeResult;
}
TourLedger oldLedger = fetchCurrentUserMatterLedger(matter.getId());
OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger);
@@ -627,6 +648,15 @@ public class TourSignupController {
return Result.success(sql.getList(Sys_dict.class));
}
/**
* 报名提交接口,PC 和 H5 共用。
* 提交成功后写入或更新疗休养台账,并回填当前用户已存在的人员分配记录事项快照。
*
* @param ledger 报名台账主信息
* @param families 家属信息 JSON 数组
* @param directRelative 直系亲属线路报名信息 JSON
* @return 报名或修改结果;直系亲属线路、超额报销等场景会返回待审核提示
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
@@ -668,6 +698,10 @@ public class TourSignupController {
TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId());
boolean directFamilyLine = line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine());
TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId());
Result boardingPlaceResult = normalizeBoardingPlace(ledger, matter, setting);
if (boardingPlaceResult != null) {
return boardingPlaceResult;
}
if (setting != null && Boolean.FALSE.equals(setting.getFillBedInfo())) {
clearBedInfo(ledger, familyList);
}
@@ -707,6 +741,8 @@ public class TourSignupController {
} else {
tourLedgerService.insert(ledger);
}
// 报名台账落库后,仅回填已存在的人员分配记录事项信息,不新增记录、不改变分配来源。
tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), ledger.getBoardingPlace());
if (Lang.isNotEmpty(familyList)) {
familyList.forEach(item -> {
item.setLedgerId(ledger.getId());
@@ -754,9 +790,21 @@ public class TourSignupController {
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
clearAssignmentMatterAfterCancel(ledger);
return Result.success().addMsg("取消报名成功");
}
private void clearAssignmentMatterAfterCancel(TourLedger ledger) {
if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) {
return;
}
TourMatter matter = tourMatterService.fetch(ledger.getMatterId());
if (matter == null || StrUtil.isBlank(matter.getSettingId())) {
return;
}
tourUserAssignmentService.clearExistingAssignmentMatterAfterCancel(matter.getSettingId(), SecurityUtil.getUserId());
}
private Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId) {
Cnd cnd = Cnd.NEW();
cnd.and("m.delFlag", "=", false);
@@ -883,19 +931,13 @@ public class TourSignupController {
if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) {
return Result.error("报名事项配置不存在或已停用");
}
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
return Result.error("您不在本次疗休养报名范围内");
Result timeResult = checkSignupTime(matter);
if (timeResult != null) {
return timeResult;
}
if (StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime())) {
return Result.error("报名时间未配置");
}
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
if (now.compareTo(normalizeDateTime(matter.getSignupStartTime(), false)) < 0) {
return Result.error("报名未开始");
}
if (now.compareTo(normalizeDateTime(matter.getSignupEndTime(), true)) > 0) {
return Result.error("报名已结束");
Result scopeResult = checkSignupEligibilityScope(setting);
if (scopeResult != null) {
return scopeResult;
}
CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger);
if (!cycleAllowedTimes.canApply()) {
@@ -906,7 +948,7 @@ public class TourSignupController {
return Result.error(buildCycleTotalCostNotice(cycleTotalCost));
}
String jobNo = currentJobNo();
if (isOutProvinceLine(line.getLineType())) {
if (!isAssignedUserEligibility(setting) && isOutProvinceLine(line.getLineType())) {
int startYear = setting.getCycleStartYear() == null ? matter.getYear() : setting.getCycleStartYear();
int endYear = LocalDate.now().getYear();
Cnd outProvinceCnd = Cnd.where(TourLedger::getDelFlag, "=", false)
@@ -1111,6 +1153,101 @@ public class TourSignupController {
return "当年报名人数";
}
private boolean isAssignedUserEligibility(TourSetting setting) {
return setting != null && "ASSIGNED_USER".equals(setting.getSignupEligibilityMode());
}
/**
* 统一校验疗休养事项报名时间,确保资格名单校验在时间校验通过后再执行。
*/
private Result checkSignupTime(TourMatter matter) {
if (matter == null || StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime())) {
return Result.error("报名时间未配置");
}
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
if (now.compareTo(normalizeDateTime(matter.getSignupStartTime(), false)) < 0) {
return Result.error("报名未开始");
}
if (now.compareTo(normalizeDateTime(matter.getSignupEndTime(), true)) > 0) {
return Result.error("报名已结束");
}
return null;
}
/**
* 报名乘车地点以疗休养配置中的乘车地点列表为准。
* 页面未传值时优先使用事项默认乘车地点,避免历史页面或代入默认值时丢失乘车地点。
*/
private Result normalizeBoardingPlace(TourLedger ledger, TourMatter matter, TourSetting setting) {
if (ledger == null || setting == null) {
return null;
}
List<String> options = parseBoardingPlaceOptions(setting.getBoardingPlace());
if (Lang.isEmpty(options)) {
ledger.setBoardingPlace("");
return null;
}
String boardingPlace = StrUtil.blankToDefault(ledger.getBoardingPlace(), matter == null ? "" : matter.getDefaultBoardingPlace());
if (StrUtil.isBlank(boardingPlace)) {
return Result.error("请选择乘车地点");
}
if (!options.contains(boardingPlace)) {
return Result.error("乘车地点不在当前疗休养配置范围内");
}
ledger.setBoardingPlace(boardingPlace);
return null;
}
private List<String> parseBoardingPlaceOptions(String value) {
if (StrUtil.isBlank(value)) {
return Collections.emptyList();
}
try {
List<?> list = Json.fromJson(List.class, value);
if (Lang.isEmpty(list)) {
return Collections.emptyList();
}
return list.stream().map(item -> {
if (item instanceof String) {
return ((String) item).trim();
}
if (item instanceof java.util.Map) {
Object name = ((java.util.Map<?, ?>) item).get("name");
return name == null ? "" : String.valueOf(name).trim();
}
return "";
})
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
} catch (Exception e) {
return java.util.Arrays.stream(value.split(","))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
}
}
/**
* 根据疗休养配置的报名资格方式切换校验来源。
* ASSIGNED_USER 模式读取人员分配表,并要求当前用户为正式人员;否则沿用可参加人员范围校验。
*/
private Result checkSignupEligibilityScope(TourSetting setting) {
if (isAssignedUserEligibility(setting)) {
NutMap assignmentResult = tourUserAssignmentService.checkFormalAssignedUser(setting.getId(), SecurityUtil.getUserId());
if (!assignmentResult.getBoolean("canApply")) {
return Result.error(defaultIfBlank(assignmentResult.getString("message"), "您不在本次疗休养人员分配名单中,无法报名"));
}
return null;
}
Integer activityGroupId = parseInteger(setting.getActivityGroupId());
if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) {
return Result.error("您不在本次疗休养报名范围内");
}
return null;
}
private String buildOutProvinceQuotaNotice(OutProvinceQuota quota) {
String suffix = quota.canApply() ? "您可以报名,或您稍后报名" : "请稍后报名";
if ("当年报名人数".equals(quota.ratioType())) {
@@ -0,0 +1,103 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养退出申请。
* 仅作为不能参加人员的维护台账,不直接关联报名、疗休养台账、人员分配等业务流程。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_leave_apply")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("疗休养退出申请")
public class TourLeaveApply extends BaseModel implements Serializable {
public static final String STATUS_PENDING = "PENDING";
public static final String STATUS_APPROVED = "APPROVED";
public static final String STATUS_REJECTED = "REJECTED";
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("疗休养事项ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String matterId;
@Column
@Comment("线路ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String lineId;
@Column
@Comment("工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String jobNo;
@Column
@Comment("姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String userName;
@Column
@Comment("用户ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("所属工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("所属工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("不能参加原因")
@ColDefine(type = ColType.TEXT)
private String reason;
@Column
@Comment("状态")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String status;
@Column
@Comment("审核人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String auditBy;
@Column
@Comment("审核人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String auditName;
@Column
@Comment("审核时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String auditTime;
@Column
@Comment("审核备注")
@ColDefine(type = ColType.TEXT)
private String auditRemark;
}
@@ -111,6 +111,11 @@ public class TourLedger extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 100)
private String travelAgencyName;
@Column
@Comment("乘车地点")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String boardingPlace;
@Column
@Comment("是否携带家属")
@ColDefine(type = ColType.BOOLEAN)
@@ -71,6 +71,11 @@ public class TourMatter extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 32)
private String lineId;
@Column
@Comment("默认乘车地点")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String defaultBoardingPlace;
@Column
@Comment("报名开始时间")
@ColDefine(type = ColType.VARCHAR, width = 20)
@@ -43,11 +43,28 @@ public class TourSetting extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 50)
private String tourType;
@Column
@Comment("乘车地点列表JSON")
@ColDefine(type = ColType.TEXT)
private String boardingPlace;
@Column
@Comment("出行人数指标")
@ColDefine(type = ColType.INT)
@Default("0")
private Integer travelPeopleQuota;
@Column
@Comment("可参加人员范围ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String activityGroupId;
@Column
@Comment("报名资格校验方式")
@ColDefine(type = ColType.VARCHAR, width = 30)
@Default("'ASSIGNED_USER'")
private String signupEligibilityMode;
@Column
@Comment("排序编号")
@ColDefine(type = ColType.INT)
@@ -127,6 +144,11 @@ public class TourSetting extends BaseModel implements Serializable {
@Many(field = "settingId")
private List<TourSettingLot> lots;
/**
* 分工会名额分配,仅用于配置弹窗回显与提交,不作为 tour_setting 表字段保存。
*/
private List<TourSettingUnionQuota> unionQuotas;
@Column
@Comment("服务须知")
@ColDefine(type = ColType.TEXT)
@@ -0,0 +1,66 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Default;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养配置下的分工会名额分配。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_setting_union_quota")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("疗休养配置分工会名额分配")
public class TourSettingUnionQuota extends BaseModel implements Serializable {
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("所属疗休养配置ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String settingId;
@Column
@Comment("分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("分工会名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("正式人员名额")
@ColDefine(type = ColType.INT)
@Default("0")
private Integer formalQuota;
@Column
@Comment("替补人员名额")
@ColDefine(type = ColType.INT)
@Default("0")
private Integer backupQuota;
/**
* 页面展示用实时会员数,不落库。
*/
private Integer memberCount;
}
@@ -0,0 +1,155 @@
package com.budwk.app.zhgh.dayofficework.tour.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Default;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableMeta;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
/**
* 疗休养人员分配表。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("tour_user_assignment")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("疗休养人员分配")
public class TourUserAssignment extends BaseModel implements Serializable {
public static final String ASSIGN_SOURCE_SCHOOL_UNION = "SCHOOL_UNION";
public static final String ASSIGN_SOURCE_BRANCH_UNION = "BRANCH_UNION";
public static final String PERSON_TYPE_FORMAL = "FORMAL";
public static final String PERSON_TYPE_BACKUP = "BACKUP";
@Column
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("疗休养配置ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String settingId;
@Column
@Comment("疗休养事项ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String matterId;
@Column
@Comment("疗休养事项名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String matterName;
@Column
@Comment("旅行社ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String travelAgencyId;
@Column
@Comment("旅行社名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String travelAgencyName;
@Column
@Comment("线路ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String lineId;
@Column
@Comment("线路名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String lineName;
@Column
@Comment("乘车地点")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String boardingPlace;
@Column
@Comment("人员ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String userId;
@Column
@Comment("工号")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String loginName;
@Column
@Comment("姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String userName;
@Column
@Comment("性别")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String gender;
@Column
@Comment("手机号")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String mobile;
@Column
@Comment("所在单位ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unitId;
@Column
@Comment("所在单位")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unitName;
@Column
@Comment("所在分工会ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String unionId;
@Column
@Comment("所在分工会")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String unionName;
@Column
@Comment("分配来源")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String assignSource;
@Column
@Comment("人员类型")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String personType;
@Column
@Comment("是否已退出")
@ColDefine(type = ColType.BOOLEAN)
@Default("0")
private Boolean cancelled;
@Column
@Comment("分配人ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
private String assignedBy;
@Column
@Comment("分配人姓名")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String assignedName;
@Column
@Comment("分配时间")
@ColDefine(type = ColType.VARCHAR, width = 30)
private String assignedAt;
}
@@ -0,0 +1,31 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourLeaveApply;
import org.nutz.lang.util.NutMap;
public interface TourLeaveApplyService extends BaseService<TourLeaveApply> {
/**
* 分页查询人员分配表中的退出/取消管理数据,关键词同时匹配工号和姓名。
* 数据权限:系统管理员、校工会主席看全部;分工会主席看本分工会;普通用户看本人。
*/
Pagination<NutMap> pageData(PageForm pageForm, String keyword, String unionName, String status);
/**
* 查询当前登录人在退出取消管理页的按钮权限。
*/
NutMap permissionInfo();
/**
* 取消指定人员分配记录,同时删除对应报名台账并标记已退出。
*/
void cancelAssignment(String id);
/**
* 恢复指定人员分配记录的退出状态,不恢复已删除台账。
*/
void restoreAssignment(String id);
}
@@ -2,6 +2,41 @@ package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingUnionQuota;
import java.util.List;
public interface TourSettingService extends BaseService<TourSetting> {
/**
* 查询所有分工会在指定疗休养配置下的名额,实时补充当前工会会员数供页面分配时参考。
*
* @param settingId 疗休养配置ID,新增时可为空
* @return 按分工会编码排序后的名额列表
*/
List<TourSettingUnionQuota> listUnionQuotaRows(String settingId);
/**
* 保存指定疗休养配置下的分工会名额,前端传入的是 JSON 数组字符串。
*
* @param settingId 疗休养配置ID
* @param unionQuotas 分工会名额 JSON
*/
void saveUnionQuotas(String settingId, String unionQuotas);
/**
* 校验分工会正式人员名额合计是否超过分工会总名额。
*
* @param unionQuotas 分工会名额 JSON
* @param branchTotalQuota 分工会总名额
* @return 通过返回空字符串;不通过返回业务提示
*/
String checkBranchFormalQuotaLimit(String unionQuotas, int branchTotalQuota);
/**
* 清理指定疗休养配置下的分工会名额。
*
* @param settingId 疗休养配置ID
*/
void clearUnionQuotas(String settingId);
}
@@ -0,0 +1,285 @@
package com.budwk.app.zhgh.dayofficework.tour.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.param.PageForm;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment;
import org.nutz.lang.util.NutMap;
import java.util.List;
public interface TourUserAssignmentService extends BaseService<TourUserAssignment> {
/**
* 分页查询校工会人员分配记录,列表只读取人员分配表,不读取报名台账。
*
* @param pageForm 分页、排序参数
* @param year 疗休养年度
* @param settingId 疗休养配置ID
* @param matterId 疗休养事项ID
* @param unionId 所属分工会ID
* @param personType 人员类型:FORMAL 正式人员,BACKUP 替补人员
* @param keyword 姓名或工号关键字
* @return 校工会分配记录分页数据
*/
Pagination<NutMap> schoolAssignmentPage(PageForm pageForm, Integer year, String settingId, String matterId,
String unionId, String personType, String keyword);
/**
* 分页查询校工会可分配候选人,候选人来自疗休养配置的可参加人员范围,并排除同一配置下已分配人员。
*
* @param pageForm 分页、排序参数
* @param settingId 疗休养配置ID
* @param unionId 所属分工会ID
* @param keyword 姓名或工号关键字
* @return 可分配候选人分页数据
*/
Pagination<NutMap> schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword);
/**
* 分页查询当前登录人所在分工会的人员分配记录,列表只读取人员分配表,不读取报名台账。
*
* @param pageForm 分页、排序参数
* @param year 疗休养年度
* @param settingId 疗休养配置ID
* @param matterId 疗休养事项ID
* @param personType 人员类型:FORMAL 正式人员,BACKUP 替补人员
* @param keyword 姓名或工号关键字
* @return 分工会分配记录分页数据
*/
Pagination<NutMap> branchAssignmentPage(PageForm pageForm, Integer year, String settingId, String matterId,
String personType, String keyword);
/**
* 分页查询当前登录人所在分工会可分配候选人,候选人来自疗休养配置的可参加人员范围。
*
* @param pageForm 分页、排序参数
* @param settingId 疗休养配置ID
* @param keyword 姓名或工号关键字
* @return 当前分工会可分配候选人分页数据
*/
Pagination<NutMap> branchCandidatePage(PageForm pageForm, String settingId, String keyword);
/**
* 查询启用的疗休养配置选项,供人员分配列表筛选和分配弹窗复用。
*
* @param year 疗休养年度
* @return 配置选项列表
*/
List<NutMap> listEnabledSettingOptions(Integer year);
/**
* 查询指定配置下已配置线路的分配线路选项,用于人员分配时指定事项、线路和旅行社。
*
* @param settingId 疗休养配置ID
* @return 事项选项列表
*/
List<NutMap> listMatterOptions(String settingId);
/**
* 查询分工会选项,供校工会分配候选人和列表筛选使用。
*
* @return 分工会选项列表
*/
List<NutMap> listUnionOptions();
/**
* 保存校工会人员分配。保存前会重新按配置可参加人员范围过滤,防止写入范围外人员。
* 分配线路为可选;正式人员选择线路时视为代报名并同步写入疗休养台账,替补人员不能分配线路。
*
* @param settingId 疗休养配置ID
* @param matterId 疗休养事项ID,可为空
* @param personType 人员类型:FORMAL 正式人员,BACKUP 替补人员
* @param userIds 待分配人员ID列表
* @return 保存结果,包含实际分配数量、跳过数量和代报名台账写入数量
*/
NutMap assignSchoolUsers(String settingId, String matterId, String personType, List<String> userIds);
/**
* 保存校工会人员分配,支持每个候选人员单独选择分配线路。
* assignItems 中每项包含 userId、matterIdmatterId 为空时只保存人员分配,不写台账。
*
* @param settingId 疗休养配置ID
* @param assignItems 人员和分配线路明细
* @return 保存结果,包含实际分配数量、跳过数量和代报名台账写入数量
*/
NutMap assignSchoolUsers(String settingId, List<NutMap> assignItems);
/**
* 给校工会已分配的正式人员补选分配线路,并按代报名写入疗休养台账。
* 只处理 SCHOOL_UNION 来源且尚未选择线路的分配记录,避免覆盖既有台账。
*
* @param id 人员分配记录ID
* @param matterId 疗休养事项ID
* @return 处理结果,包含台账写入数量
*/
NutMap selectSchoolAssignmentMatter(String id, String matterId);
/**
* 保存分工会人员分配。保存前会重新按配置可参加人员范围和当前登录人所在分工会过滤,并校验正式/替补名额。
* 疗休养事项为可选;正式人员选择事项时视为代报名并同步写入疗休养台账,替补人员不能分配事项。
*
* @param settingId 疗休养配置ID
* @param matterId 疗休养事项ID,可为空
* @param personType 人员类型:FORMAL 正式人员,BACKUP 替补人员
* @param userIds 待分配人员ID列表
* @return 保存结果,包含实际分配数量、跳过数量、代报名台账写入数量和剩余名额
*/
NutMap assignBranchUsers(String settingId, String matterId, String personType, List<String> userIds);
/**
* 切换当前分工会人员分配记录的正式/替补状态。
* 切换时会校验当前登录人所在分工会、分配来源和目标类型剩余名额;已分配事项的正式人员不能切换为替补。
*
* @param id 人员分配记录ID
* @param personType 目标人员类型:FORMAL 正式人员,BACKUP 替补人员
* @return 切换后的人员类型和最新名额信息
*/
NutMap switchCurrentBranchPersonType(String id, String personType);
/**
* 用户报名写入台账后,回填该用户在同一疗休养配置下已存在的人员分配记录。
* 仅更新事项、线路、旅行社快照字段,不新增记录,不修改 assignSource 和人员类型。
*
* @param settingId 疗休养配置ID
* @param matterId 疗休养事项ID
* @param userId 报名用户ID
* @param boardingPlace 用户报名时最终选择的乘车地点
* @return 更新记录数
*/
int backfillExistingAssignmentAfterSignup(String settingId, String matterId, String userId, String boardingPlace);
/**
* 用户取消报名删除台账后,清空该用户在同一疗休养配置下已存在人员分配记录的事项快照。
* 仅清空事项、线路、旅行社字段,不删除记录,不修改 assignSource 和人员类型。
*
* @param settingId 疗休养配置ID
* @param userId 报名用户ID
* @return 更新记录数
*/
int clearExistingAssignmentMatterAfterCancel(String settingId, String userId);
/**
* 查询当前登录人所在分工会在指定疗休养配置下的名额使用情况。
*
* @param settingId 疗休养配置ID
* @return 正式/替补名额、已用名额和剩余名额
*/
NutMap branchQuotaInfo(String settingId);
/**
* 查询校工会人员分配记录是否已存在对应报名台账,用于删除前二次确认。
*
* @param id 人员分配记录ID
* @return hasLedger 表示是否存在台账,ledgerCount 表示对应台账数量
*/
NutMap schoolDeleteInfo(String id);
/**
* 查询当前分工会人员分配记录是否已存在对应报名台账,用于删除前二次确认。
*
* @param id 人员分配记录ID
* @return hasLedger 表示是否存在台账,ledgerCount 表示对应台账数量
*/
NutMap branchDeleteInfo(String id);
/**
* 删除指定来源的人员分配记录,避免校工会页面误删分工会分配的数据。
*
* @param id 人员分配记录ID
* @param assignSource 分配来源
*/
void deleteBySource(String id, String assignSource);
/**
* 删除指定来源的人员分配记录,可选择是否同步删除该分配记录对应的报名台账。
*
* @param id 人员分配记录ID
* @param assignSource 分配来源
* @param deleteLedger 是否同步删除 matterId + loginName 对应的台账及明细
*/
void deleteBySource(String id, String assignSource, boolean deleteLedger);
/**
* 删除当前登录人所在分工会的人员分配记录,避免分工会页面删除其它分工会的数据。
*
* @param id 人员分配记录ID
*/
void deleteCurrentBranchAssignment(String id);
/**
* 删除当前登录人所在分工会的人员分配记录,可选择是否同步删除该分配记录对应的报名台账。
*
* @param id 人员分配记录ID
* @param deleteLedger 是否同步删除 matterId + loginName 对应的台账及明细
*/
void deleteCurrentBranchAssignment(String id, boolean deleteLedger);
/**
* 判断用户是否已存在于指定疗休养配置的人员分配表中。
*
* @param settingId 疗休养配置ID
* @param userId 用户ID
* @return 存在返回 true,不存在返回 false
*/
boolean existsAssignedUser(String settingId, String userId);
/**
* 校验用户是否为指定疗休养配置下的正式分配人员。
* 用于报名资格方式为 ASSIGNED_USER 时,拦截未分配人员和替补人员。
* 已退出人员也会被拦截,避免取消后再次报名。
*
* @param settingId 疗休养配置ID
* @param userId 用户ID
* @return canApply 表示是否可报名,message 表示不可报名原因
*/
NutMap checkFormalAssignedUser(String settingId, String userId);
/**
* 实时统计指定疗休养配置下校工会来源的正式分配人数。
* 已退出人员不再占用工会名额,统计时需要排除。
*
* @param settingId 疗休养配置ID
* @return 当前配置下校工会正式人员分配数量
*/
int countSchoolFormalAssignedUsers(String settingId);
/**
* 将人员分配记录标记为已退出,并删除该记录对应事项下的报名台账。
* 取消前会校验路线出行开始时间,只有出行开始前指定天数之前允许取消。
*
* @param id 人员分配记录ID
*/
void cancelAssignment(String id);
/**
* 将人员分配记录从已退出恢复为未退出。
* 只恢复人员分配表状态,不自动恢复已删除的报名台账。
*
* @param id 人员分配记录ID
*/
void restoreCancelledAssignment(String id);
/**
* 按分配来源恢复已退出人员,避免校工会和分工会页面互相恢复对方数据。
*
* @param id 人员分配记录ID
* @param assignSource 分配来源
*/
void restoreCancelledAssignmentBySource(String id, String assignSource);
/**
* 恢复当前登录人所在分工会的已退出人员分配记录。
* 用于分工会人员分配列表的“取消退出”按钮,防止跨分工会恢复数据。
*
* @param id 人员分配记录ID
*/
void restoreCurrentBranchCancelledAssignment(String id);
/**
* 按疗休养配置清理人员分配数据,供删除配置或后续重置分配时复用。
*
* @param settingId 疗休养配置ID
*/
void clearBySettingId(String settingId);
}
@@ -0,0 +1,169 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
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.dayofficework.tour.models.TourLeaveApply;
import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLeaveApplyService;
import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
@IocBean(args = {"refer:dao"})
public class TourLeaveApplyServiceImpl extends BaseServiceImpl<TourLeaveApply> implements TourLeaveApplyService {
@Inject
private TourUserAssignmentService tourUserAssignmentService;
public TourLeaveApplyServiceImpl(Dao dao) {
super(dao);
}
@Override
public Pagination<NutMap> pageData(PageForm pageForm, String keyword, String unionName, String status) {
Sql sql = Sqls.create("""
SELECT
a.*,
s.`year` AS `year`,
s.configName AS settingName,
CASE
WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> ''
THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)
ELSE ''
END AS travelPeriod,
m.travelStartTime AS travelStartTime,
IF(IFNULL(a.cancelled, 0) = 1, 'CANCELLED', 'NORMAL') AS cancelStatus
FROM tour_user_assignment a
LEFT JOIN tour_setting s ON s.id = a.settingId
LEFT JOIN tour_matter m ON m.id = a.matterId AND m.delFlag = 0
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("a.delFlag", "=", false);
// 退出取消只针对已选路线的数据,未分配路线的人员没有出行时间和台账可处理。
cnd.and("a.matterId", "is not", null);
cnd.and("a.matterId", "<>", "");
cnd.and("a.lineId", "is not", null);
cnd.and("a.lineId", "<>", "");
cnd.and("a.personType", "=", TourUserAssignment.PERSON_TYPE_FORMAL);
applyDataPermission(cnd);
if (StrUtil.isNotBlank(keyword)) {
SqlExpressionGroup keywordGroup = new SqlExpressionGroup();
keywordGroup.orLike("a.loginName", keyword);
keywordGroup.orLike("a.userName", keyword);
cnd.and(keywordGroup);
}
cnd.and(Cnd.likeEX("a.unionName", unionName));
if ("CANCELLED".equals(status)) {
cnd.and("a.cancelled", "=", true);
} else if ("NORMAL".equals(status)) {
cnd.and(Cnd.exps("a.cancelled", "=", false).or("a.cancelled", "is", null));
}
applyOrder(cnd, pageForm);
sql.setCondition(cnd);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
public NutMap permissionInfo() {
return NutMap.NEW()
.addv("isAllDataRole", isAllDataRole())
.addv("isBranchUnionChairman", AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name()))
.addv("canRestore", StpUtil.hasPermission("tour.branchUserAssignment.cancelRestore"));
}
@Override
public void cancelAssignment(String id) {
checkCanOperateAssignment(id);
tourUserAssignmentService.cancelAssignment(id);
}
@Override
public void restoreAssignment(String id) {
checkCanOperateAssignment(id);
tourUserAssignmentService.restoreCancelledAssignment(id);
}
/**
* 退出取消管理读取人员分配表,按用户角色收窄可见范围。
*/
private void applyDataPermission(Cnd cnd) {
if (isAllDataRole()) {
return;
}
if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
cnd.andEX("a.unionId", "=", SecurityUtil.getUnionId());
return;
}
cnd.and("a.userId", "=", SecurityUtil.getUserId());
}
private boolean isAllDataRole() {
return AuthUtil.hasRole(RoleConstant.SYSADMIN.name())
|| AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_CHAIRMAN.name());
}
/**
* 操作前按同一数据权限校验,避免前端绕过列表直接提交其它人员记录。
*/
private void checkCanOperateAssignment(String id) {
if (StrUtil.isBlank(id)) {
throw new IllegalArgumentException("参数错误");
}
Cnd cnd = Cnd.where(TourUserAssignment::getId, "=", id)
.and(TourUserAssignment::getDelFlag, "=", false);
if (!isAllDataRole()) {
if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
cnd.and(TourUserAssignment::getUnionId, "=", SecurityUtil.getUnionId());
} else {
cnd.and(TourUserAssignment::getUserId, "=", SecurityUtil.getUserId());
}
}
if (dao().count(TourUserAssignment.class, cnd) <= 0) {
throw new IllegalArgumentException("人员分配记录不存在或无权操作");
}
}
/**
* 仅开放页面使用到的排序字段,避免前端传入任意字段名影响查询。
*/
private void applyOrder(Cnd cnd, PageForm pageForm) {
String orderName = pageForm.getPageOrderName();
String orderBy = pageForm.getPageOrderBy();
boolean descending = "descending".equals(orderBy);
if ("loginName".equals(orderName)) {
order(cnd, "a.loginName", descending);
} else if ("userName".equals(orderName)) {
order(cnd, "a.userName", descending);
} else if ("unionName".equals(orderName)) {
order(cnd, "a.unionName", descending);
} else if ("cancelStatus".equals(orderName)) {
order(cnd, "a.cancelled", descending);
} else if ("assignedAt".equals(orderName)) {
order(cnd, "a.assignedAt", descending);
} else {
cnd.desc("a.assignedAt");
cnd.desc("a.createdAt");
}
}
private void order(Cnd cnd, String field, boolean descending) {
if (descending) {
cnd.desc(field);
} else {
cnd.asc(field);
}
}
}
@@ -1,10 +1,23 @@
package com.budwk.app.zhgh.dayofficework.tour.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_union;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting;
import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingUnionQuota;
import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.util.NutMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class TourSettingServiceImpl extends BaseServiceImpl<TourSetting> implements TourSettingService {
@@ -12,4 +25,109 @@ public class TourSettingServiceImpl extends BaseServiceImpl<TourSetting> impleme
public TourSettingServiceImpl(Dao dao) {
super(dao);
}
@Override
public List<TourSettingUnionQuota> listUnionQuotaRows(String settingId) {
// 一次性查出所有分工会、已保存名额和实时会员数,避免页面打开时按分工会循环统计造成 N+1 查询。
Sql sql = Sqls.create("""
SELECT
quota.id AS id,
@settingId AS settingId,
un.id AS unionId,
un.name AS unionName,
COALESCE(quota.formalQuota, 0) AS formalQuota,
COALESCE(quota.backupQuota, 0) AS backupQuota,
COALESCE(user_count.memberCount, 0) AS memberCount
FROM sys_union un
LEFT JOIN tour_setting_union_quota quota
ON quota.unionId = un.id
AND quota.settingId = @settingId
LEFT JOIN (
SELECT unionId, COUNT(1) AS memberCount
FROM vw_user
WHERE member = 1
GROUP BY unionId
) user_count ON user_count.unionId = un.id
ORDER BY un.unionCode ASC
""");
sql.setParam("settingId", settingId == null ? "" : settingId);
List<NutMap> rows = listMap(sql);
return rows.stream().map(row -> {
TourSettingUnionQuota quota = new TourSettingUnionQuota();
quota.setId(row.getString("id"));
quota.setSettingId(row.getString("settingId"));
quota.setUnionId(row.getString("unionId"));
quota.setUnionName(row.getString("unionName"));
quota.setFormalQuota(defaultInt(row.getInt("formalQuota")));
quota.setBackupQuota(defaultInt(row.getInt("backupQuota")));
quota.setMemberCount(defaultInt(row.getInt("memberCount")));
return quota;
}).collect(Collectors.toList());
}
@Override
public void saveUnionQuotas(String settingId, String unionQuotas) {
if (StrUtil.isBlank(settingId)) {
return;
}
clearUnionQuotas(settingId);
if (StrUtil.isBlank(unionQuotas)) {
return;
}
List<TourSettingUnionQuota> quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas);
if (Lang.isEmpty(quotaList)) {
return;
}
Map<String, Sys_union> unionMap = dao().query(Sys_union.class, Cnd.NEW()).stream()
.collect(Collectors.toMap(Sys_union::getId, item -> item, (a, b) -> a));
List<TourSettingUnionQuota> saveList = quotaList.stream()
.filter(item -> item != null && StrUtil.isNotBlank(item.getUnionId()))
.map(item -> normalizeUnionQuota(settingId, item, unionMap))
.filter(item -> item.getFormalQuota() > 0 || item.getBackupQuota() > 0)
.collect(Collectors.toList());
if (Lang.isNotEmpty(saveList)) {
dao().insert(saveList);
}
}
@Override
public String checkBranchFormalQuotaLimit(String unionQuotas, int branchTotalQuota) {
if (StrUtil.isBlank(unionQuotas)) {
return "";
}
List<TourSettingUnionQuota> quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas);
if (Lang.isEmpty(quotaList)) {
return "";
}
int formalTotal = quotaList.stream()
.filter(item -> item != null)
.mapToInt(item -> defaultInt(item.getFormalQuota()))
.sum();
if (formalTotal > branchTotalQuota) {
return "当前正式人员总数 " + formalTotal + ",分工会总名额 " + branchTotalQuota + ",分配后总人数不能超过分工会总名额";
}
return "";
}
@Override
public void clearUnionQuotas(String settingId) {
if (StrUtil.isNotBlank(settingId)) {
dao().clear(TourSettingUnionQuota.class, Cnd.where(TourSettingUnionQuota::getSettingId, "=", settingId));
}
}
private TourSettingUnionQuota normalizeUnionQuota(String settingId, TourSettingUnionQuota item, Map<String, Sys_union> unionMap) {
Sys_union union = unionMap.get(item.getUnionId());
TourSettingUnionQuota quota = new TourSettingUnionQuota();
quota.setSettingId(settingId);
quota.setUnionId(item.getUnionId());
quota.setUnionName(union == null ? item.getUnionName() : union.getName());
quota.setFormalQuota(defaultInt(item.getFormalQuota()));
quota.setBackupQuota(defaultInt(item.getBackupQuota()));
return quota;
}
private Integer defaultInt(Integer value) {
return value == null || value < 0 ? 0 : value;
}
}