This commit is contained in:
2026-06-12 09:28:09 +08:00
parent 67bf620d01
commit 742a0ff388
13 changed files with 591 additions and 95 deletions
@@ -119,6 +119,21 @@ public class TourBranchUserAssignmentController {
}
}
/**
* 给当前分工会已分配正式人员补选分配路线,具体快照回写、代报名和最大成团人数校验由 service 统一处理。
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("tour.branchUserAssignment")
@SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "选择分工会分配事项")
public Result selectMatter(String id, String matterId) {
try {
return Result.success(tourUserAssignmentService.selectCurrentBranchAssignmentMatter(id, matterId));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
/**
* 恢复当前分工会已取消退出的人员分配记录,只恢复状态,不恢复已删除台账。
*/
@@ -29,6 +29,7 @@ import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService;
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.TourUserAssignmentService;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.BorderStyle;
@@ -93,6 +94,9 @@ public class TourLedgerController {
@Inject
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
@Inject
private TourUserAssignmentService tourUserAssignmentService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/ledger/index.html")
@SaCheckPermission("tour.ledger")
@@ -234,8 +238,11 @@ public class TourLedgerController {
if (!canAccessLedgerUnion(ledger.getUnionId())) {
return Result.error("无权删除该台账记录");
}
// 删除台账时同步清理台账明细和人员分配中的线路快照,避免人员分配列表继续显示已删除报名的路线信息。
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", id));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", id));
tourLedgerService.delete(id);
tourUserAssignmentService.clearExistingAssignmentMatterAfterLedgerDelete(ledger.getMatterId(), ledger.getJobNo());
return Result.success();
}
@@ -139,11 +139,13 @@ public class TourSignupController {
@At("/h5/pageData")
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result h5PageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId, Boolean directFamilyUnitLine) {
public Result h5PageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId,
String travelPeriod, Boolean directFamilyUnitLine) {
if (StrUtil.isBlank(pageForm.getPageOrderName())) {
pageForm.defaultSortAsc("lineName");
}
Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId);
appendTravelPeriodCnd(cnd, travelPeriod);
cnd.andEX("l.directFamilyUnitLine", "=", directFamilyUnitLine);
String currentJobNo = currentJobNo();
@@ -372,6 +374,34 @@ public class TourSignupController {
return Result.success(sql.getList(NutMap.class));
}
/**
* 查询移动端选择路线页的出行时间筛选项,按 yyyyMMdd-yyyyMMdd 格式拼接事项出行起止时间并去重。
*/
@At("/h5/travelPeriodOptions")
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result h5TravelPeriodOptions(Integer year, String lineType, Boolean directFamilyUnitLine) {
Cnd cnd = buildQueryCnd(year, null, null, null);
cnd.andEX("l.lineType", "=", lineType);
cnd.andEX("l.directFamilyUnitLine", "=", directFamilyUnitLine);
cnd.and("m.travelStartTime", "IS NOT", null);
cnd.and("m.travelStartTime", "<>", "");
cnd.and("m.travelEndTime", "IS NOT", null);
cnd.and("m.travelEndTime", "<>", "");
Sql sql = Sqls.create("""
SELECT DISTINCT $travelPeriodExpr AS travelPeriod
FROM tour_matter m
INNER JOIN tour_line l ON l.id = m.lineId
LEFT JOIN sys_union u ON u.id = m.unionId
$condition
ORDER BY travelPeriod ASC
""");
sql.setVar("travelPeriodExpr", travelPeriodKeyExpr());
sql.setCondition(cnd);
sql.setCallback(Sqls.callback.maps());
tourMatterService.dao().execute(sql);
return Result.success(sql.getList(NutMap.class));
}
@At
@SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR)
public Result lineDetail(String lineId) {
@@ -820,6 +850,18 @@ public class TourSignupController {
return cnd;
}
private void appendTravelPeriodCnd(Cnd cnd, String travelPeriod) {
if (cnd == null || StrUtil.isBlank(travelPeriod)) {
return;
}
// 移动端出行时间筛选使用 yyyyMMdd-yyyyMMdd 作为稳定值,避免前端直接依赖数据库原始时间格式。
cnd.and(travelPeriodKeyExpr(), "=", travelPeriod);
}
private String travelPeriodKeyExpr() {
return "CONCAT(REPLACE(SUBSTRING(m.travelStartTime, 1, 10), '-', ''), '-', REPLACE(SUBSTRING(m.travelEndTime, 1, 10), '-', ''))";
}
private Result checkSignup(TourLedger ledger) {
if (ledger == null) {
return Result.error("参数错误");
@@ -15,6 +15,7 @@ import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily;
import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService;
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.TourUserAssignmentService;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
@@ -58,6 +59,9 @@ public class TourUnionLedgerController {
@Inject
private TourLedgerDirectRelativeService tourLedgerDirectRelativeService;
@Inject
private TourUserAssignmentService tourUserAssignmentService;
@At("")
@Ok("beetl:/platform/zhgh/dayofficework/Tour/unionledger/index.html")
@SaCheckPermission("tour.unionledger")
@@ -206,8 +210,11 @@ public class TourUnionLedgerController {
if (ledger == null) {
return Result.error("台账记录不存在或无权删除");
}
// 删除分工会台账时同步清理台账明细和人员分配中的线路快照,避免人员分配列表继续显示已删除报名的路线信息。
tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()));
tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()));
tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId()));
tourUserAssignmentService.clearExistingAssignmentMatterAfterLedgerDelete(ledger.getMatterId(), ledger.getJobNo());
return Result.success();
}
@@ -115,6 +115,16 @@ public interface TourUserAssignmentService extends BaseService<TourUserAssignmen
*/
NutMap selectSchoolAssignmentMatter(String id, String matterId);
/**
* 给当前分工会已分配的正式人员补选分配路线,并按代报名写入疗休养台账。
* 只处理当前登录人所在分工会、BRANCH_UNION 来源且尚未选择路线的记录;写台账前校验最多成团人数。
*
* @param id 人员分配记录ID
* @param matterId 疗休养事项ID
* @return 处理结果,包含台账写入数量
*/
NutMap selectCurrentBranchAssignmentMatter(String id, String matterId);
/**
* 保存分工会人员分配。保存前会重新按配置可参加人员范围和当前登录人所在分工会过滤,并校验正式/替补名额。
* 疗休养事项为可选;正式人员选择事项时视为代报名并同步写入疗休养台账,替补人员不能分配事项。
@@ -159,6 +169,16 @@ public interface TourUserAssignmentService extends BaseService<TourUserAssignmen
*/
int clearExistingAssignmentMatterAfterCancel(String settingId, String userId);
/**
* 台账管理删除报名台账后,按原台账的事项和工号清空对应人员分配记录的事项快照。
* 仅清空事项、线路、旅行社和乘车地点字段;出行时间由事项关联展示,清空事项后页面不再显示原出行时间。
*
* @param matterId 台账原报名事项ID
* @param jobNo 台账原报名人工号
* @return 更新记录数
*/
int clearExistingAssignmentMatterAfterLedgerDelete(String matterId, String jobNo);
/**
* 查询当前登录人所在分工会在指定疗休养配置下的名额使用情况。
*
@@ -310,20 +310,38 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl<TourUserAssig
if (matterInfo == null) {
throw new IllegalArgumentException("分配线路不存在或未配置线路");
}
// 补选事项时同时回写人员分配表事项快照,后续列表筛选无需再跨表追溯事项信息。
update(Chain.make("matterId", matterInfo.getString("matterId"))
.add("matterName", matterInfo.getString("matterName"))
.add("lineId", matterInfo.getString("lineId"))
.add("lineName", matterInfo.getString("lineName"))
.add("travelAgencyId", matterInfo.getString("travelAgencyId"))
.add("travelAgencyName", matterInfo.getString("travelAgencyName"))
.add("boardingPlace", matterInfo.getString("defaultBoardingPlace")),
Cnd.where(TourUserAssignment::getId, "=", id)
.and(TourUserAssignment::getAssignSource, "=", TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION));
updateAssignmentMatterSnapshot(id, matterInfo, TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION);
int ledgerCount = insertProxySignupLedgers(matterInfo, Collections.singletonList(fetchAssignmentUserSnapshot(assignment)));
return NutMap.NEW().addv("ledgerCount", ledgerCount);
}
@Override
public NutMap selectCurrentBranchAssignmentMatter(String id, String matterId) {
if (StrUtil.isBlank(id) || StrUtil.isBlank(matterId)) {
throw new IllegalArgumentException("请选择人员分配记录和分配路线");
}
TourUserAssignment assignment = fetchCurrentBranchAssignment(id);
if (assignment == null) {
throw new IllegalArgumentException("人员分配记录不存在");
}
if (StrUtil.isNotBlank(assignment.getMatterId())) {
throw new IllegalArgumentException("该人员已选择分配路线");
}
String personType = normalizePersonType(assignment.getPersonType());
if (!TourUserAssignment.PERSON_TYPE_FORMAL.equals(personType)) {
throw new IllegalArgumentException("只有正式人员可以选择分配路线");
}
NutMap matterInfo = fetchMatterInfo(assignment.getSettingId(), matterId);
if (matterInfo == null) {
throw new IllegalArgumentException("分配路线不存在或未配置线路");
}
List<NutMap> users = Collections.singletonList(fetchAssignmentUserSnapshot(assignment));
checkProxySignupMaxGroupPeople(matterInfo, users);
updateAssignmentMatterSnapshot(id, matterInfo, TourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION);
int ledgerCount = insertProxySignupLedgers(matterInfo, users);
return NutMap.NEW().addv("ledgerCount", ledgerCount);
}
@Override
public NutMap assignBranchUsers(String settingId, String matterId, String personType, List<String> userIds) {
String unionId = SecurityUtil.getUnionId();
@@ -358,6 +376,8 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl<TourUserAssig
if (candidateUsers.size() > remainingQuota) {
throw new IllegalArgumentException("选择人数超过当前分工会剩余名额");
}
// 分工会代报名会占用线路成团人数,人员分配和台账写入前先做后端兜底校验。
checkProxySignupMaxGroupPeople(matterInfo, candidateUsers);
List<TourUserAssignment> assignments = candidateUsers.stream()
.map(user -> buildAssignment(settingId, matterInfo, normalizedPersonType, user,
TourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION))
@@ -506,6 +526,29 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl<TourUserAssig
.and(TourUserAssignment::getDelFlag, "=", false));
}
@Override
public int clearExistingAssignmentMatterAfterLedgerDelete(String matterId, String jobNo) {
if (StrUtil.isBlank(matterId) || StrUtil.isBlank(jobNo)) {
return 0;
}
TourMatter matter = dao().fetch(TourMatter.class, matterId);
Cnd cnd = Cnd.where(TourUserAssignment::getMatterId, "=", matterId)
.and(TourUserAssignment::getLoginName, "=", jobNo)
.and(TourUserAssignment::getDelFlag, "=", false);
if (matter != null && StrUtil.isNotBlank(matter.getSettingId())) {
cnd.and(TourUserAssignment::getSettingId, "=", matter.getSettingId());
}
// 台账管理按原台账删除时无法依赖当前登录人,需用台账工号精准清空同事项的人员分配快照。
return update(Chain.make("matterId", null)
.add("matterName", null)
.add("lineId", null)
.add("lineName", null)
.add("travelAgencyId", null)
.add("travelAgencyName", null)
.add("boardingPlace", null),
cnd);
}
@Override
public NutMap schoolDeleteInfo(String id) {
return assignmentDeleteInfo(fetchAssignmentBySource(id, TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION));
@@ -867,6 +910,7 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl<TourUserAssig
m.`year` AS `year`,
m.settingId,
m.defaultBoardingPlace,
m.maxGroupPeople,
m.lineId,
l.lineName,
l.lineType,
@@ -928,6 +972,19 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl<TourUserAssig
assignment.setBoardingPlace(matterInfo.getString("defaultBoardingPlace"));
}
private void updateAssignmentMatterSnapshot(String id, NutMap matterInfo, String assignSource) {
// 补选事项时同时回写人员分配表事项快照,后续列表筛选无需再跨表追溯事项信息。
update(Chain.make("matterId", matterInfo.getString("matterId"))
.add("matterName", matterInfo.getString("matterName"))
.add("lineId", matterInfo.getString("lineId"))
.add("lineName", matterInfo.getString("lineName"))
.add("travelAgencyId", matterInfo.getString("travelAgencyId"))
.add("travelAgencyName", matterInfo.getString("travelAgencyName"))
.add("boardingPlace", matterInfo.getString("defaultBoardingPlace")),
Cnd.where(TourUserAssignment::getId, "=", id)
.and(TourUserAssignment::getAssignSource, "=", assignSource));
}
private NutMap prepareMatterInfoForAssignment(String settingId, String matterId, String personType) {
if (TourUserAssignment.PERSON_TYPE_BACKUP.equals(personType) && StrUtil.isNotBlank(matterId)) {
throw new IllegalArgumentException("替补人员不能分配疗休养事项");
@@ -958,6 +1015,52 @@ public class TourUserAssignmentServiceImpl extends BaseServiceImpl<TourUserAssig
return ledgers.size();
}
private void checkProxySignupMaxGroupPeople(NutMap matterInfo, List<NutMap> users) {
if (matterInfo == null || Lang.isEmpty(users)) {
return;
}
Integer maxGroupPeople = matterInfo.getInt("maxGroupPeople");
if (maxGroupPeople == null || maxGroupPeople <= 0) {
return;
}
String matterId = matterInfo.getString("matterId");
List<NutMap> newSignupUsers = users.stream()
// 已存在同事项台账的人员不会重复代报名,也不再计入本次新增人数。
.filter(user -> !existsMatterLedger(matterId, user.getString("loginName")))
.collect(Collectors.toList());
if (Lang.isEmpty(newSignupUsers)) {
return;
}
int signedPeople = countMatterSignupPeople(matterId);
int totalPeople = signedPeople + newSignupUsers.size();
if (totalPeople > maxGroupPeople) {
throw new IllegalArgumentException("当前报名人数 " + signedPeople + " 人,本次代报名 "
+ newSignupUsers.size() + " 人,最多成团人数 " + maxGroupPeople + " 人,报名后将超过人数上限,不能提交");
}
}
private int countMatterSignupPeople(String matterId) {
if (StrUtil.isBlank(matterId)) {
return 0;
}
Sql sql = Sqls.create("""
SELECT IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS signupPeople
FROM tour_ledger t
LEFT JOIN (
SELECT ledgerId, COUNT(1) AS familyCount
FROM tour_ledger_family
WHERE delFlag = 0
GROUP BY ledgerId
) f ON f.ledgerId = t.id
WHERE t.delFlag = 0
AND t.matterId = @matterId
""");
sql.setParam("matterId", matterId);
sql.setCallback(Sqls.callback.integer());
dao().execute(sql);
return sql.getInt();
}
private boolean existsMatterLedger(String matterId, String loginName) {
if (StrUtil.isBlank(matterId) || StrUtil.isBlank(loginName)) {
return false;