diff --git a/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyActivityApplyController.java b/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyActivityApplyController.java index 1213702..72c4e71 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyActivityApplyController.java +++ b/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyActivityApplyController.java @@ -521,6 +521,12 @@ public class FamilyActivityApplyController { } } + /** + * 提交当前登录教职工的报名,不接受客户端指定报名用户或已有报名ID。 + * @param familyUser courseId为子活动ID,mobile为联系方式,mobileColumnsValue为家属动态字段二维列表, + * 独立报名课程还需activityCourseId时段ID;活动归属及用户由service确定 + * @return Result JSON,code为0表示正常或候补报名成功,msg说明结果或未通过的规则 + */ @At @ApiOperation("活动报名") @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) @@ -529,37 +535,7 @@ public class FamilyActivityApplyController { public Result doSignUp(FamilyUser familyUser) { try { lock.lock(); - FamilyActivity activity = dao.fetch(FamilyActivity.class, familyUser.getActivityId()); - if(Lang.isEmpty(familyUser.getMobileColumnsValue())) { - return Result.error(99,"请填写" + activity.getKeyWord() + "信息"); - } - boolean courseByUser = familyActivityService.isSignCourseByUser(familyUser.getCourseId(), SecurityUtil.getUserId()); - if(courseByUser) { - return Result.error(99,"您已报过该活动"); - } - - Map validFamilyCount = familyActivityService.validFamilyCount(familyUser); - if(validFamilyCount.containsKey(false)) { - return Result.error(99,validFamilyCount.get(false)); - } - // 判断人数 - int number = 0; - FamilyCourse course = familyActivityService.dao().fetch(FamilyCourse.class, familyUser.getCourseId()); - FamilyType type = familyActivityService.dao().fetch(FamilyType.class, course.getCourseType()); - if(type != null) { - if(type.getIsBringFamily() && type.getIsAddFamily()) { - List> mobileColumnsValue = familyUser.getMobileColumnsValue(); - number = Lang.isNotEmpty(mobileColumnsValue) ? mobileColumnsValue.size() : 0; - } - } - boolean signFull = familyActivityService.isSignFull(course, number); - if(signFull) { - return Result.error(99,"当前报名人数已满"); - } - boolean signFullByUnionId = familyActivityService.isSignFullByUnionId(course, number); - if(signFullByUnionId) { - return Result.error(99,"该活动您所在的分工会名额不足"); - } + // 家属、名额和时间冲突由service在数据库锁及事务内统一校验,避免普通报名与快速报名规则分叉。 FamilyUser finishUser = familyActivityService.doSignUp(familyUser); if(finishUser.getState() == 2) { return Result.success("正式报名已满,您已列为替补,如有老师放弃报名,将会按先后顺序依次替补。请您自行关注后续结果。谢谢!"); diff --git a/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyActivityController.java b/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyActivityController.java index 915cec6..c34b1f0 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyActivityController.java +++ b/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyActivityController.java @@ -13,6 +13,8 @@ import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.nutz.dao.Chain; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; @@ -144,11 +146,18 @@ public class FamilyActivityController { return Result.success().addData(dataMap); } + /** + * 保存或发布活动配置,业务校验由service执行。 + * @param activity 活动及子活动列表;familyCountScope为1按类型、2按活动,timeConflictCheck为重叠校验开关, + * onlyKey为各类型共有且必填的字段编码,familyMaxCount为活动累计上限 + * @return Result JSON,code为0表示保存成功,配置不完整时返回具体业务错误 + */ @At @Ok("json:full") @ApiOperation("亲子活动新增/修改") @SaCheckPermission("family.manage") @SLog(tag = "亲子活动-活动管理", msg = "新增/修改活动") + @Aop(TransAop.READ_COMMITTED) public Result doHandle(FamilyActivity activity) { if (StrUtil.isBlank(activity.getId())) { familyActivityManageService.add(activity, null); diff --git a/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyAdjustController.java b/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyAdjustController.java index bc085d1..ea9b244 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyAdjustController.java +++ b/src/main/java/com/budwk/app/zhgh/activity/family/controller/manage/FamilyAdjustController.java @@ -1,8 +1,10 @@ package com.budwk.app.zhgh.activity.family.controller.manage; import cn.dev33.satoken.annotation.SaCheckPermission; -import cn.hutool.core.date.DateUtil; import com.budwk.app.base.annotation.SLog; +import com.budwk.app.base.exception.BaseException; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import com.budwk.app.base.page.Pagination; import com.budwk.app.base.param.PageForm; import com.budwk.app.base.result.Result; @@ -24,7 +26,6 @@ import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; -import java.util.ArrayList; import java.util.List; /** @@ -118,59 +119,44 @@ public class FamilyAdjustController { return Result.success(list); } + /** + * @param activityId 当前活动ID + * @param oldCourseId 人员原报名子活动ID + * @param newCourseId 同活动内的目标子活动ID + * @param userId 被调整教职工ID,按该人员而非管理员校验家属和时间规则 + * @return Result JSON;code为0表示调整成功,失败时msg说明原因且保留原报名 + */ @At @ApiOperation("人员调整") @SaCheckPermission("family.adjust") @SLog(tag = "亲子活动-人员调整", msg = "人员调整") + @Aop(TransAop.READ_COMMITTED) public Result adjust(String activityId, String oldCourseId, String newCourseId, String userId) { - - Cnd oldCnd = Cnd.where("activityId", "=", activityId) - .and("courseId", "=", oldCourseId).and("userId", "=", userId); - - Cnd newCnd = Cnd.where("activityId", "=", activityId) - .and("courseId", "=", newCourseId).and("userId", "=", userId); - - //旧的报名信息 - FamilyUser oldfamilyUser = dao.fetch(FamilyUser.class, oldCnd); - oldfamilyUser.setCourseId(newCourseId); - oldfamilyUser.setSignUpTime(DateUtil.date()); - dao.update(oldfamilyUser); - - //新的课程的信息,上课时间 - List activityCourseList = dao.query(FamilyActivityCourse.class, Cnd.where("activityId", "=", activityId) - .and("courseId", "=", newCourseId)); - //先清楚旧的信息 - dao.clear(FamilyUserCourse.class, oldCnd); - //添加新的信息 - List familyUserCourseList = new ArrayList<>(); - activityCourseList.forEach(item -> { - FamilyUserCourse course = new FamilyUserCourse(); - course.setActivityCourseId(activityId); - course.setCourseId(newCourseId); - course.setUserId(userId); - course.setCourseStartTime(item.getCourseStartTime()); - course.setCourseEndTime(item.getCourseEndTime()); - course.setActivityCourseId(item.getId()); - familyUserCourseList.add(course); - }); - dao.insert(familyUserCourseList); - return Result.success(); + try { + familyActivityManageService.adjustSignUp(activityId, oldCourseId, newCourseId, userId); + return Result.success(); + } catch (BaseException e) { + return Result.error(99, e.getMessage()); + } } + /** + * @param activityId 当前活动ID + * @param courseId 待删除报名所属子活动ID + * @param userId 被删除报名的教职工ID + * @return Result JSON;code为0表示对应报名及其时段记录已删除 + */ @At @ApiOperation("删除报名人员") @SaCheckPermission("family.adjust") @SLog(tag = "亲子活动-人员调整", msg = "删除报名人员") + @Aop(TransAop.READ_COMMITTED) public Result deleteSignUser(String activityId, String courseId, String userId) { - - //删除 - Cnd cnd = Cnd.NEW(); - cnd.and("activityId", "=", activityId); - cnd.and("courseId", "=", courseId); - cnd.and("userId", "=", userId); - - dao.clear(FamilyUser.class, cnd); - dao.clear(FamilyUserCourse.class, cnd); - return Result.success(); + try { + familyActivityManageService.deleteSignUser(activityId, courseId, userId); + return Result.success(); + } catch (BaseException e) { + return Result.error(99, e.getMessage()); + } } } diff --git a/src/main/java/com/budwk/app/zhgh/activity/family/models/FamilyActivity.java b/src/main/java/com/budwk/app/zhgh/activity/family/models/FamilyActivity.java index 2788996..c27bf72 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/family/models/FamilyActivity.java +++ b/src/main/java/com/budwk/app/zhgh/activity/family/models/FamilyActivity.java @@ -127,6 +127,18 @@ public class FamilyActivity extends BaseModel implements Serializable, SysHomeCo @ColDefine(type = ColType.INT) private Integer familyMaxCount; + @Column + @Comment("家属人数限制范围:1按子活动类型累计,2按整个活动累计;历史空值按1处理") + @ColDefine(type = ColType.INT) + @Default("1") + private Integer familyCountScope; + + @Column + @Comment("是否禁止同一教职工报名本活动内时间重叠的子活动,含候补报名") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean timeConflictCheck; + @Column @Comment("判断家属数量的唯一标识") @ColDefine(type = ColType.VARCHAR, width = 50) diff --git a/src/main/java/com/budwk/app/zhgh/activity/family/service/FamilyActivityService.java b/src/main/java/com/budwk/app/zhgh/activity/family/service/FamilyActivityService.java index 8b7f43d..865c3dc 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/family/service/FamilyActivityService.java +++ b/src/main/java/com/budwk/app/zhgh/activity/family/service/FamilyActivityService.java @@ -76,6 +76,25 @@ public interface FamilyActivityService extends BaseService { */ void cancelSignUp(String activityId, String courseId); + /** + * 后台将指定教职工的正常报名调整到同一活动的另一子活动,复用家属及时间冲突规则。 + * @param activityId 活动ID,原课程与目标课程必须属于该活动 + * @param oldCourseId 原课程ID + * @param newCourseId 目标课程ID;独立时段课程须只有一个可确定的时段 + * @param userId 被调整教职工的用户ID,不使用管理员身份参与统计 + * @return 无返回值;不满足限制时抛出业务异常,原报名不变 + */ + void adjustSignUp(String activityId, String oldCourseId, String newCourseId, String userId); + + /** + * 后台删除指定教职工的报名,与报名和调班共用数据库锁,避免并发统计读取到中间状态。 + * @param activityId 活动ID + * @param courseId 课程ID + * @param userId 被删除报名的教职工ID + * @return 无返回值;仅删除匹配的报名及其课程时段记录 + */ + void deleteSignUser(String activityId, String courseId, String userId); + /** * 查询当前用户在指定课程中的预填信息。 * @@ -120,7 +139,8 @@ public interface FamilyActivityService extends BaseService { FamilyUser getSignUpInfo(String courseId, String userId); /** - * 校验当前用户是否可以修改指定课程的报名信息。 + * 打开编辑表单前校验当前用户报名归属、报名时间和人员范围。 + * 家属、人数和时间冲突在保存新数据时重新校验,允许历史冲突记录进入表单整改。 * * @param courseId 课程ID;当前用户必须已经报名该课程 * @return 无返回值;校验不通过时抛出包含具体原因的业务异常 @@ -166,7 +186,7 @@ public interface FamilyActivityService extends BaseService { boolean isSignFull(FamilyCourse course, Integer currentFamilyNumber); /** - * 校验本次提交的家属数量、唯一标识及分场冲突。 + * 校验本次提交的类型家属上限、活动累计家属上限、唯一标识及分场冲突。 * 当参数包含报名记录ID时,会排除该记录后再校验,用于修改报名。 * * @param user 待校验的报名信息 diff --git a/src/main/java/com/budwk/app/zhgh/activity/family/service/impl/FamilyActivityServiceImpl.java b/src/main/java/com/budwk/app/zhgh/activity/family/service/impl/FamilyActivityServiceImpl.java index 85fa906..1dec796 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/family/service/impl/FamilyActivityServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/activity/family/service/impl/FamilyActivityServiceImpl.java @@ -48,10 +48,74 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i super(dao); } + /** + * 新增、沿用和编辑活动共用配置校验。activity 为页面提交的完整活动,oldActivity 为编辑前记录。 + * 唯一标识必须使用所有课程类型共有且必填的字段,不限定验证规则,不改写历史字段编码。 + * 返回值为空;字段缺失、编码不一致或历史数据可能失联时抛出可定位类型的业务异常。 + */ + private void validateActivityRuleConfig(FamilyActivity activity, FamilyActivity oldActivity) { + if (activity.getFamilyCountScope() == null) { + activity.setFamilyCountScope(1); + } + if (activity.getFamilyCountScope() != 1 && activity.getFamilyCountScope() != 2) { + throw new BaseException("家属人数限制范围不正确"); + } + boolean activityScope = Objects.equals(activity.getFamilyCountScope(), 2); + if (activityScope && (activity.getFamilyMaxCount() == null || activity.getFamilyMaxCount() < 1)) { + throw new BaseException("按整个活动累计时,家属最多数必须为正整数"); + } + boolean keyChanged = oldActivity == null || !Objects.equals(oldActivity.getOnlyKey(), activity.getOnlyKey()); + if (oldActivity != null && keyChanged + && (dao().count(FamilyUser.class, Cnd.where("activityId", "=", activity.getId())) > 0 + || dao().count(FamilyApplyPrefill.class, Cnd.where("activityId", "=", activity.getId())) > 0)) { + throw new BaseException("活动已有报名或预填信息,不能直接更换唯一标识,请先核对历史数据"); + } + List courses = activity.getCourseList(); + if (Lang.isEmpty(courses)) { + if (activityScope || Boolean.TRUE.equals(activity.getTimeConflictCheck())) { + throw new BaseException("启用报名规则前请先配置子活动"); + } + return; + } + // 已选唯一标识即检查所有参与类型,即使编码未更换也必须满足共有与必填要求。 + boolean checkKey = activityScope || StrUtil.isNotBlank(activity.getOnlyKey()); + if (activityScope && StrUtil.isBlank(activity.getOnlyKey())) { + throw new BaseException("请选择各子活动类型共有且必填的字段作为唯一标识"); + } + for (FamilyCourse course : courses) { + if (checkKey) { + FamilyType type = StrUtil.isBlank(course.getCourseType()) ? null + : dao().fetch(FamilyType.class, course.getCourseType()); + if (type == null) { + throw new BaseException("请为子活动配置有效类型:" + course.getCourseName()); + } + List columns = dao().query(FamilyMobileSignColumn.class, + Cnd.where("typeId", "=", type.getId()).and("columnCode", "=", activity.getOnlyKey())); + if (columns.size() != 1 || !Boolean.TRUE.equals(columns.get(0).getIsRequired())) { + throw new BaseException("类型【" + type.getTypeName() + "】必须配置同编码且必填的唯一标识字段:" + + activity.getOnlyKey()); + } + } + if (Boolean.TRUE.equals(activity.getTimeConflictCheck())) { + if (Lang.isEmpty(course.getCourseTimeList())) { + throw new BaseException("启用时间冲突校验前请配置子活动时间:" + course.getCourseName()); + } + for (FamilyActivityCourse time : course.getCourseTimeList()) { + if (time == null || time.getCourseDate() == null || time.getCourseStartTime() == null + || time.getCourseEndTime() == null + || DateUtil.format(time.getCourseStartTime(), "HH:mm").compareTo( + DateUtil.format(time.getCourseEndTime(), "HH:mm")) >= 0) { + throw new BaseException("子活动日期或起止时间不完整、无效:" + course.getCourseName()); + } + } + } + } + } + @Override @Aop(TransAop.READ_COMMITTED) public void add(FamilyActivity activity, FamilyCourse course) { - + validateActivityRuleConfig(activity, null); dao().insert(activity); //插入类型限制 @@ -81,7 +145,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i @Override @Aop(TransAop.READ_COMMITTED) public void edit(FamilyActivity activity) { - + validateActivityRuleConfig(activity, dao().fetch(FamilyActivity.class, activity.getId())); //修改活动 update(activity); @@ -155,6 +219,8 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i } } + /** 将页面的课程日期与时分合成为实际上课时间,保存到同一事务的课程时段表。 */ + @Aop(TransAop.READ_COMMITTED) private void setCourseTimeAndInsert(FamilyCourse course) { course.getCourseTimeList().forEach(courseTime -> { Calendar calendar = Calendar.getInstance(); @@ -254,7 +320,13 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i @Aop(TransAop.READ_COMMITTED) public FamilyUser doSignUp(FamilyUser familyUser) throws Exception { String userId = SecurityUtil.getUserId(); - + if (familyUser == null) { + throw new BaseException("报名信息不能为空"); + } + // 先锁教职工,再锁课程;跨课程报名也必须串行校验同一活动的家属总数和时间。 + lockSignUpUser(userId); + familyUser.setId(null); + familyUser.setUserId(userId); // 对课程行加数据库锁,使名额校验和正式写入在多节点部署时仍按顺序执行。 lockCourseForSignUp(familyUser.getCourseId()); //查询课程 @@ -272,6 +344,10 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId()); validateSignUpTimeAndScope(activity, userId, false); familyUser.setActivityId(course.getActivityId()); + if (!Boolean.TRUE.equals(course.getCourseIsLimitApply())) { + // 非独立时段课程必须参加全部时段,忽略客户端多传的单个时段ID。 + familyUser.setActivityCourseId(null); + } if (getSignUpInfo(course.getId(), userId) != null) { throw new BaseException("抱歉,您已经报名"); } @@ -298,6 +374,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i : "您选择的类型已达上限,不能再报该类型的了"); } validateCourseTime(course, familyUser.getActivityCourseId(), null); + validateTimeConflict(familyUser, course, activity, userId); //如果这个课程的预留名额方式为报名人数不变 if (course.getReserveMode() == 2) { //如果当前报名+已报小于这个课程限制人数 @@ -345,6 +422,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i throw new BaseException("取消报名参数不完整"); } + lockSignUpUser(SecurityUtil.getUserId()); // 与正式报名共用课程行锁,保证删除、重新统计和候补转正在同一串行区间内完成。 lockCourseForSignUp(courseId); FamilyCourse course = dao().fetch(FamilyCourse.class, courseId); @@ -378,6 +456,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i * 按报名时间顺序从全部分工会候补中查找当前可容纳的人员。 * 本人和家属是否计入总人数,严格按课程类型配置计算。 */ + @Aop(TransAop.READ_COMMITTED) private void promoteWaitingUsers(FamilyCourse course) { FamilyType type = dao().fetch(FamilyType.class, course.getCourseType()); if (type == null) { @@ -459,6 +538,78 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i && DateUtil.compare(now, activity.getActivitySignUpEndTime()) < 0; } + @Override + @Aop(TransAop.READ_COMMITTED) + public void adjustSignUp(String activityId, String oldCourseId, String newCourseId, String userId) { + if (StrUtil.hasBlank(activityId, oldCourseId, newCourseId, userId)) { + throw new BaseException("人员调整参数不完整"); + } + lockSignUpUser(userId); + // 调班涉及两门课,固定按ID排序加锁,避免不同教职工反向调班时死锁。 + List.of(oldCourseId, newCourseId).stream().distinct().sorted().forEach(this::lockCourseForSignUp); + FamilyUser original = dao().fetch(FamilyUser.class, Cnd.where("activityId", "=", activityId) + .and("courseId", "=", oldCourseId).and("userId", "=", userId)); + if (original == null || (!Objects.equals(original.getState(), 1) && !Objects.equals(original.getState(), 3))) { + throw new BaseException("只允许调整当前有效的正常报名"); + } + FamilyCourse source = dao().fetch(FamilyCourse.class, oldCourseId); + FamilyCourse target = dao().fetch(FamilyCourse.class, newCourseId); + if (!Objects.equals(source.getActivityId(), activityId) || !Objects.equals(target.getActivityId(), activityId)) { + throw new BaseException("原子活动和目标子活动必须属于当前活动"); + } + if (Objects.equals(oldCourseId, newCourseId)) { + return; + } + if (getSignUpInfo(newCourseId, userId) != null) { + throw new BaseException("该教职工已经报名目标子活动"); + } + FamilyActivity activity = dao().fetch(FamilyActivity.class, activityId); + FamilyType type = dao().fetch(FamilyType.class, target.getCourseType()); + if (activity == null || type == null) { + throw new BaseException("活动或目标子活动类型不存在"); + } + original.setCourseId(newCourseId); + original.setActivityCourseId(null); + if (Boolean.TRUE.equals(target.getCourseIsLimitApply())) { + List times = dao().query(FamilyActivityCourse.class, Cnd.where("courseId", "=", newCourseId)); + if (times.size() != 1) { + throw new BaseException("目标子活动包含多个独立报名时段,请通过报名表单明确选择时段"); + } + original.setActivityCourseId(times.get(0).getId()); + } + original.setMobileColumnsValue(normalizeAndValidateFamilyColumns(original.getMobileColumnsValue(), type, target, activity)); + Map result = validFamilyCount(original, userId); + if (result.containsKey(false)) { + throw new BaseException(result.get(false)); + } + validateTimeConflict(original, target, activity, userId); + validateCourseTime(target, original.getActivityCourseId(), null); + int contribution = getFamilyContribution(type, original.getMobileColumnsValue().size()); + if (statisticsService.queryCourseCount(newCourseId, target.getCourseType()) + contribution + + target.getCourseReservedNumber() > target.getCoursePeopleNumber() || target.getCoursePeopleNumber() == 0) { + throw new BaseException("目标子活动名额不足"); + } + original.setSignUpTime(new Date()); + dao().update(original); + // 调班后的所有校验通过才删除旧时段,异常时事务回滚,保留原报名。 + dao().clear(FamilyUserCourse.class, Cnd.where("activityId", "=", activityId) + .and("courseId", "=", oldCourseId).and("userId", "=", userId)); + rebuildUserCourseTimes(original); + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void deleteSignUser(String activityId, String courseId, String userId) { + if (StrUtil.hasBlank(activityId, courseId, userId)) { + throw new BaseException("删除报名参数不完整"); + } + lockSignUpUser(userId); + lockCourseForSignUp(courseId); + Cnd cnd = Cnd.where("activityId", "=", activityId).and("courseId", "=", courseId).and("userId", "=", userId); + dao().clear(FamilyUserCourse.class, cnd); + dao().clear(FamilyUser.class, cnd); + } + @Override public FamilyApplyPrefill getPrefill(String courseId, String userId) { if (StrUtil.isBlank(courseId) || StrUtil.isBlank(userId)) { @@ -581,7 +732,13 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i if (currentSignUp == null) { throw new BaseException("未找到本人在该课程的报名信息"); } - validateUpdateSignUpData(currentSignUp, currentSignUp); + // 打开编辑表单只校验归属和报名时间,让历史冲突数据仍可进入表单整改;保存时校验新数据。 + FamilyCourse course = dao().fetch(FamilyCourse.class, courseId); + if (course == null || !Objects.equals(course.getActivityId(), currentSignUp.getActivityId())) { + throw new BaseException("报名课程不存在或已变更"); + } + validateSignUpTimeAndScope(dao().fetch(FamilyActivity.class, course.getActivityId()), + SecurityUtil.getUserId(), true); } @Override @@ -592,6 +749,8 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i } String currentUserId = SecurityUtil.getUserId(); + lockSignUpUser(currentUserId); + lockCourseForSignUp(familyUser.getCourseId()); FamilyUser currentSignUp = getSignUpInfo(familyUser.getCourseId(), currentUserId); if (currentSignUp == null || !Objects.equals(currentSignUp.getId(), familyUser.getId())) { throw new BaseException("未找到本人在该课程的报名信息"); @@ -617,6 +776,9 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i } FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId()); validateSignUpTimeAndScope(activity, SecurityUtil.getUserId(), true); + if (!Boolean.TRUE.equals(course.getCourseIsLimitApply())) { + familyUser.setActivityCourseId(null); + } FamilyType type = dao().fetch(FamilyType.class, course.getCourseType()); if (type == null) { throw new BaseException("报名类型不存在或已变更"); @@ -651,6 +813,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i : "您选择的类型已达上限,不能再报该类型的了"); } validateCourseTime(course, familyUser.getActivityCourseId(), currentSignUp); + validateTimeConflict(familyUser, course, activity, SecurityUtil.getUserId()); } @Override @@ -701,6 +864,22 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i } } + /** + * 锁定实际报名教职工的账号行,覆盖多节点、PC/H5和快速报名的跨课程并发。 + * userId 必须由登录态或已校验的后台人员调整参数提供,所有写入口统一先锁账号再锁课程。 + */ + private void lockSignUpUser(String userId) { + if (StrUtil.isBlank(userId)) { + throw new BaseException("报名人员不能为空"); + } + Sql sql = Sqls.create("SELECT id FROM sys_user WHERE id = @userId FOR UPDATE"); + sql.setParam("userId", userId); + sql.setCallback(Sqls.callback.str()); + if (dao().execute(sql).getResult() == null) { + throw new BaseException("报名人员不存在或已删除"); + } + } + /** * 预填新增、修改和删除共用时间及人员范围校验。 * 预填只允许发生在报名开始时间之前,到达开始时间后数据立即冻结。 @@ -775,6 +954,14 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i for (FamilyMobileSignColumn config : columnConfigs) { NutMap submittedColumn = submittedValueMap.get(config.getColumnCode()); Object columnValue = submittedColumn == null ? null : submittedColumn.get("columnValue"); + if (Objects.equals(activity.getFamilyCountScope(), 2) + && Objects.equals(activity.getOnlyKey(), config.getColumnCode())) { + if (!Boolean.TRUE.equals(config.getIsRequired())) { + throw new BaseException("活动累计家属人数需要同编码且必填的唯一标识字段,请联系管理员核对类型配置"); + } + // 姓名等普通字段保留大小写;仅有效身份证号统一末位X,历史统计采用相同规则。 + columnValue = normalizeFamilyIdentityValue(columnValue == null ? null : String.valueOf(columnValue)); + } validateDynamicColumnValue(config, columnValue, familyIndex, course, activity); NutMap normalizedColumn = NutMap.NEW(); @@ -986,13 +1173,104 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i } } + /** 提取活动指定字段的值;活动级累计去除首尾空格,普通字段保留大小写,历史按类型规则保持原值。 */ + private String getFamilyIdentity(List family, FamilyActivity activity) { + if (family == null) { + return null; + } + String value = family.stream().filter(Objects::nonNull) + .filter(column -> Objects.equals(activity.getOnlyKey(), column.getString("columnCode"))) + .map(column -> column.getString("columnValue")).filter(Objects::nonNull).findFirst().orElse(null); + return value != null && Objects.equals(activity.getFamilyCountScope(), 2) + ? normalizeFamilyIdentityValue(value) : value; + } + + /** + * 统一本次提交与历史记录的唯一标识比较值,不要求值必须为身份证。 + * value 为所选字段的文本值,返回去除首尾空格的文本;有效身份证号末位x统一为X,其他文本保持原大小写。 + */ + private String normalizeFamilyIdentityValue(String value) { + if (value == null) { + return null; + } + String identity = value.trim(); + return IdcardUtil.isValidCard(identity) ? identity.toUpperCase(Locale.ROOT) : identity; + } + + /** + * 获取一条报名实际参加的课程时段。独立报名仅取所选时段,否则取全部时段。 + * course 为所属子活动,activityCourseId 为用户所选时段ID;返回完整有效的日期时间区间列表。 + * 缺失、越权或无效时间不允许跳过冲突检查,避免缺配置成为绕过入口。 + */ + private List getSignUpCourseTimes(FamilyCourse course, String activityCourseId) { + Cnd cnd = Cnd.where(FamilyActivityCourse::getCourseId, "=", course.getId()); + if (Boolean.TRUE.equals(course.getCourseIsLimitApply())) { + if (StrUtil.isBlank(activityCourseId)) { + throw new BaseException("请选择报名时段:" + course.getCourseName()); + } + cnd.and(FamilyActivityCourse::getId, "=", activityCourseId); + } + List times = dao().query(FamilyActivityCourse.class, cnd); + if (Lang.isEmpty(times)) { + throw new BaseException("子活动报名时段不存在或未配置,请联系管理员:" + course.getCourseName()); + } + for (FamilyActivityCourse time : times) { + if (time.getCourseStartTime() == null || time.getCourseEndTime() == null + || !time.getCourseStartTime().before(time.getCourseEndTime())) { + throw new BaseException("子活动起止时间配置无效,请联系管理员:" + course.getCourseName()); + } + } + return times; + } + + /** + * 在同一活动、同一教职工内校验时间重叠,含候补,修改和调班排除当前报名记录。 + * 参数为本次报名、所属课程、活动配置和实际报名用户ID;无返回值,冲突时提示已有课程和时间。 + * 使用半开区间:一场结束时刻等于另一场开始时刻允许报名;不同日期自然不会冲突。 + */ + private void validateTimeConflict(FamilyUser user, FamilyCourse course, FamilyActivity activity, String userId) { + if (!Boolean.TRUE.equals(activity.getTimeConflictCheck())) { + return; + } + List currentTimes = getSignUpCourseTimes(course, user.getActivityCourseId()); + Cnd cnd = Cnd.where(FamilyUser::getActivityId, "=", activity.getId()) + .and(FamilyUser::getUserId, "=", userId).and(FamilyUser::getState, "in", List.of(1, 2, 3)); + if (StrUtil.isNotBlank(user.getId())) { + cnd.and(FamilyUser::getId, "!=", user.getId()); + } + for (FamilyUser existing : dao().query(FamilyUser.class, cnd)) { + FamilyCourse oldCourse = dao().fetch(FamilyCourse.class, existing.getCourseId()); + if (oldCourse == null || !Objects.equals(oldCourse.getActivityId(), activity.getId())) { + throw new BaseException("已有报名的子活动不存在或已变更,请联系管理员核对"); + } + for (FamilyActivityCourse oldTime : getSignUpCourseTimes(oldCourse, existing.getActivityCourseId())) { + for (FamilyActivityCourse currentTime : currentTimes) { + if (currentTime.getCourseStartTime().before(oldTime.getCourseEndTime()) + && oldTime.getCourseStartTime().before(currentTime.getCourseEndTime())) { + throw new BaseException("您已报名【" + oldCourse.getCourseName() + "】(" + + DateUtil.format(oldTime.getCourseStartTime(), "yyyy-MM-dd HH:mm") + "至" + + DateUtil.format(oldTime.getCourseEndTime(), "yyyy-MM-dd HH:mm") + + "),与当前报名时间重叠,不能兼报"); + } + } + } + } + } + /** * 仅在报名时段发生变化时重建本人课程时段记录,未变化时保留原签到和领取信息。 */ + @Aop(TransAop.READ_COMMITTED) private void syncUpdatedUserCourse(FamilyUser currentSignUp, String oldActivityCourseId) { if (Objects.equals(oldActivityCourseId, currentSignUp.getActivityCourseId())) { return; } + rebuildUserCourseTimes(currentSignUp); + } + + /** 修改所选时段或后台调班后,按本次报名实际参加的时段重建课程记录。 */ + @Aop(TransAop.READ_COMMITTED) + private void rebuildUserCourseTimes(FamilyUser currentSignUp) { dao().clear(FamilyUserCourse.class, Cnd.where(FamilyUserCourse::getActivityId, "=", currentSignUp.getActivityId()) .and(FamilyUserCourse::getCourseId, "=", currentSignUp.getCourseId()) .and(FamilyUserCourse::getUserId, "=", currentSignUp.getUserId())); @@ -1023,6 +1301,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i @Async @Override + @Aop(TransAop.READ_COMMITTED) public void asyncInsertUserCourse(String activityId, String courseId, String userId) { log.info("异步插入{}的上课信息,课程ID为{},活动ID为{}", userId, courseId, activityId); insertUserCourse(activityId, courseId, userId); @@ -1031,6 +1310,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i /** * 同步写入报名人员的全部课程时段,用于保证正式报名主记录与时段记录处于同一事务。 */ + @Aop(TransAop.READ_COMMITTED) private void insertUserCourse(String activityId, String courseId, String userId) { List courseList = dao().query(FamilyActivityCourse.class, Cnd.where("courseId", "=", courseId)); List list = new ArrayList<>(); @@ -1112,6 +1392,11 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i @Override public Map validFamilyCount(FamilyUser user) { + return validFamilyCount(user, SecurityUtil.getUserId()); + } + + /** 按实际报名用户校验家属集合,后台调班不能使用当前管理员身份查询报名。 */ + private Map validFamilyCount(FamilyUser user, String userId) { String activityId = user.getActivityId(); FamilyActivity activity = dao().fetch(FamilyActivity.class, activityId); if (activity == null) { @@ -1137,7 +1422,11 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i // 查询本人本活动的其他报名,修改报名时排除当前记录,后续冲突校验仍覆盖全部子活动类型。 Cnd signUpCnd = Cnd.where(FamilyUser::getActivityId, "=", activityId) - .and(FamilyUser::getUserId, "=", SecurityUtil.getUserId()); + .and(FamilyUser::getUserId, "=", userId); + if (Objects.equals(activity.getFamilyCountScope(), 2) || Boolean.TRUE.equals(activity.getTimeConflictCheck())) { + // 正常、候补及候补转正占用规则;废弃报名不占用新规则的家属名额。 + signUpCnd.and(FamilyUser::getState, "in", List.of(1, 2, 3)); + } if (StrUtil.isNotBlank(user.getId())) { signUpCnd.and(FamilyUser::getId, "!=", user.getId()); } @@ -1146,6 +1435,31 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i Map courseMap = courseList.stream() .collect(Collectors.toMap(FamilyCourse::getId, FamilyCourse::getCourseType)); + if (Objects.equals(activity.getFamilyCountScope(), 2)) { + if (activity.getFamilyMaxCount() == null || activity.getFamilyMaxCount() < 1) { + return Map.of(false, "活动未配置有效的家属最多数,请联系管理员"); + } + Set identities = new HashSet<>(); + List allSignUps = new ArrayList<>(list); + allSignUps.add(user); + for (FamilyUser signUp : allSignUps) { + if (Lang.isEmpty(signUp.getMobileColumnsValue())) { + return Map.of(false, "报名记录缺少家属信息,请联系管理员核对历史数据"); + } + for (List family : signUp.getMobileColumnsValue()) { + String identity = getFamilyIdentity(family, activity); + if (StrUtil.isBlank(identity)) { + return Map.of(false, "报名记录缺少统一的家属唯一标识,请联系管理员核对字段编码和历史数据"); + } + identities.add(identity); + } + } + if (identities.size() > activity.getFamilyMaxCount()) { + return Map.of(false, "本活动累计" + activity.getKeyWord() + "人数最多为" + activity.getFamilyMaxCount() + + ",请使用已报名的家属,不能更换为其他人"); + } + } + // 家属最多数按当前子活动类型独立累计,同类型多个子活动按家属唯一标识去重。 List> allMobileColumnsValue = new ArrayList<>(list.stream() .filter(item -> Objects.equals(currentCourse.getCourseType(), courseMap.get(item.getCourseId()))) @@ -1157,11 +1471,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i allMobileColumnsValue.addAll(user.getMobileColumnsValue()); long count = allMobileColumnsValue.stream() - .filter(Objects::nonNull) - .flatMap(List::stream) - .filter(Objects::nonNull) - .filter(nutMap -> Objects.equals(activity.getOnlyKey(), nutMap.getString("columnCode"))) - .map(nutMap -> nutMap.getString("columnValue")) + .map(family -> getFamilyIdentity(family, activity)) .filter(Objects::nonNull) .distinct() .count(); @@ -1190,7 +1500,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i continue; } // 家属匹配唯一标识的值 - String onlyKeyValue = nutMap.getString("columnValue"); + String onlyKeyValue = getFamilyIdentity(listMap, activity); if(Objects.equals(tempValue, onlyKeyValue)) { return Map.of(false, activity.getKeyWord() + "不能重复报同一分场"); } @@ -1212,7 +1522,7 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i if(hisNutMap == null) { continue; } - String hisKeyValue = hisNutMap.getString("columnValue"); + String hisKeyValue = getFamilyIdentity(hisValue, activity); // 判断是不是同一个孩子 if(Objects.equals(onlyKeyValue, hisKeyValue)) { oneFamilyCount ++; diff --git a/src/main/resources/static/components/module/trainSignUp/TrainDynamicForm.vue b/src/main/resources/static/components/module/trainSignUp/TrainDynamicForm.vue index c892a13..b97da8b 100644 --- a/src/main/resources/static/components/module/trainSignUp/TrainDynamicForm.vue +++ b/src/main/resources/static/components/module/trainSignUp/TrainDynamicForm.vue @@ -2,6 +2,17 @@ .field .van-field__error-message { display: none; } + +/* 与测量节点共用排版,确保滚轮每一行都能容纳完整选项。 */ +.train-picker-option-text { + width: 100%; + font-size: 16px; + line-height: 22px; + text-align: center; + white-space: pre-wrap; + word-break: break-all; + overflow-wrap: anywhere; +} @@ -140,7 +158,12 @@ module.exports = { name: "TrainDynamicForm", components: {}, props: { - dynamicData: [] + dynamicData: [], + // 由亲子活动报名页单独启用,其余引用页面保留原有选项展示。 + fullOptionText: { + type: Boolean, + default: false + } }, model: { prop: "dynamicData", @@ -160,6 +183,8 @@ module.exports = { return { //所有的picker状态 pickerStates: {}, + pickerItemHeights: {}, + pickerVisibleItemCounts: {}, fileList: [], addressStates: {}, addressAreaList: window.trainSignUpAreaList || { province_list: {}, city_list: {}, county_list: {} } @@ -444,7 +469,44 @@ module.exports = { const addressValue = (state.displayArea || "") + (state.detail || "") this.$set(column, "columnValue", addressValue) }, + /** + * 按当前屏幕宽度测量最长选项,统一行高以保持滚轮定位准确。 + * @param column 动态字段配置,selectValues 为后台配置的字符串选项数组 + * @return 无返回值;将像素行高和屏内可见行数写入对应字段的展示状态 + */ + measurePickerOptions(column) { + if (!this.fullOptionText || column.columnFormType !== "SELECT" + || ["DATE", "DATETIME"].includes(column.columnType)) { + return + } + const measureElement = document.createElement("div") + measureElement.className = "train-picker-option-text" + // Vant 选项左右各有 4px 内边距,测量宽度必须与实际文字区域一致。 + Object.assign(measureElement.style, { + position: "fixed", + visibility: "hidden", + pointerEvents: "none", + width: Math.max(document.documentElement.clientWidth - 8, 1) + "px", + fontFamily: window.getComputedStyle(this.$el).fontFamily + }) + document.body.appendChild(measureElement) + let itemHeight = 44 + try { + (column.selectValues || []).forEach((option) => { + measureElement.textContent = option + itemHeight = Math.max(itemHeight, Math.ceil(measureElement.getBoundingClientRect().height) + 16) + }) + } finally { + document.body.removeChild(measureElement) + } + this.$set(this.pickerItemHeights, column.columnCode, itemHeight) + // 多行选项减少同时展示的行数,避免滚轮随行高增加而撑出屏幕。 + const availableHeight = Math.min(264, Math.max(window.innerHeight - 44, 0)) + this.$set(this.pickerVisibleItemCounts, column.columnCode, + Math.max(1, Math.floor(availableHeight / itemHeight))) + }, selectFieldClick(column) { + this.measurePickerOptions(column) this.$set(this.pickerStates, column.columnCode + "Picker", true) if (!["DATE", "DATETIME"].includes(column.columnType)) { //说明是普通的下拉框 diff --git a/src/main/resources/views/platform/zhgh/activity/family/apply/signForm.js b/src/main/resources/views/platform/zhgh/activity/family/apply/signForm.js index 536fa44..b56960c 100644 --- a/src/main/resources/views/platform/zhgh/activity/family/apply/signForm.js +++ b/src/main/resources/views/platform/zhgh/activity/family/apply/signForm.js @@ -3,7 +3,7 @@ const signForm = {
+ class="family-sign-dialog" top="5vh" append-to-body>
个人信息
@@ -72,6 +72,9 @@ const signForm = {
+
+ 本活动累计最多报名{{activity.familyMaxCount}}名不同{{activity.keyWord}},含候补;多个子活动请使用已报名的家属。 +
- + 取 消 {{prefillMode ? '保存预填' : '提 交'}} @@ -158,7 +161,7 @@ const signForm = { courseRow: {}, courseTypeRow: {}, courseTimeSelectList: [], - active: '', + active: '0', activity: {}, } @@ -173,13 +176,23 @@ const signForm = { this.$message.warning('家属最多人数为' + this.courseTypeRow.familyMaxCount) return } + // 类型上限与整个活动上限同时生效;跨课程已用家属集合由后端在最终提交时校验。 + if (this.activity.familyCountScope === 2 + && this.formData.mobileColumnsValue.length >= Number(this.activity.familyMaxCount)) { + this.$message.warning('本活动累计家属人数最多为' + this.activity.familyMaxCount) + return + } const list = clone(this.courseTypeRow.familyMobileSignColumnList) this.formData.mobileColumnsValue.push(list) + // 新增儿童后选中对应的字符串索引,确保标签和填写区域同步显示。 + this.$set(this, "active", String(this.formData.mobileColumnsValue.length - 1)) }, delFamily() { + // 无儿童时不执行删除;删完后保留首项索引,兼容重新添加儿童。 + if (this.formData.mobileColumnsValue.length === 0) return this.formData.mobileColumnsValue.splice(this.active, 1) const ac = (Number(this.active) - 1) - this.active = '' + (ac >= 0 ? ac : 0) + this.$set(this, "active", String(ac >= 0 ? ac : 0)) }, validSignUp() { //获取家属是多少人 @@ -478,12 +491,33 @@ const signForm = { this.mergeMobileColumnsValue(courseType, signUpData.mobileColumnsValue)) } this.$set(this, "formData", formData) + // 不同活动共用报名组件,每次打开都选中首个儿童,避免沿用上次的标签状态。 + this.$set(this, "active", "0") this.$nextTick(() => { this.$refs.form.clearValidate() }) }, }, style: /*language=CSS*/ ` + /* 报名弹框保留上下可视边距,仅正文滚动,标题和操作按钮始终可见。 */ + .family-sign-dialog { + overflow: hidden; + } + .family-sign-dialog > .el-dialog { + display: flex; + flex-direction: column; + max-height: 90vh; + margin-bottom: 0; + } + .family-sign-dialog > .el-dialog > .el-dialog__header, + .family-sign-dialog > .el-dialog > .el-dialog__footer { + flex-shrink: 0; + } + .family-sign-dialog > .el-dialog > .el-dialog__body { + min-height: 0; + overflow-y: auto; + overscroll-behavior-y: contain; + } ::v-deep .el-tabs__header { margin: 0 0 15px !important; } diff --git a/src/main/resources/views/platform/zhgh/activity/family/manage/basicForm.js b/src/main/resources/views/platform/zhgh/activity/family/manage/basicForm.js index 7c4ea1a..f6d3284 100644 --- a/src/main/resources/views/platform/zhgh/activity/family/manage/basicForm.js +++ b/src/main/resources/views/platform/zhgh/activity/family/manage/basicForm.js @@ -114,6 +114,24 @@ const basicForm = { + + + 按子活动类型累计 + 按整个活动累计 + +
+ 同一教职工在本活动所有子活动中,家属按唯一标识去重后的累计人数不得超过家属最多数;含候补报名。 +
+
+ + +
+ 按子活动实际日期及起止时间判断,含候补报名。上午与同日全天互斥,上午与下午、不同日期可兼报。 +
+
@@ -280,9 +298,14 @@ const basicForm = {
判断家属数量唯一标识
- - + + +
+ 请选择本活动所有子活动类型共有且均设为必填的字段,各类型字段编码必须一致。有报名或预填后禁止直接更换。 +
@@ -290,8 +313,8 @@ const basicForm = { 取消 上一步 下一步 - 保存 - 提交 + 保存 + 提交 @@ -325,6 +348,7 @@ const basicForm = { data() { return { signUpDialog: false, + formLoading: false, step: 1, formData: { notice: false, @@ -348,7 +372,9 @@ const basicForm = { restrictLimit: 1, limitNum: 1, typeLimits: [], - keyWord: '家属' + keyWord: '家属', + familyCountScope: 1, + timeConflictCheck: false, }, historicalActList: [], trainTypeList: [], @@ -372,28 +398,47 @@ const basicForm = { }, methods: { keyFocus() { - const types = this.formData.courseList.filter(o => o.courseType !== undefined).map(o => o.courseType) + const types = [...new Set(this.formData.courseList.map(o => o.courseType).filter(Boolean))] if(types.length === 0) { + this.$set(this, 'typeArray', []) this.$message.warning('请先设置' + this.activityType + '类型') return } const typeList = this.courseTypeList.filter(o => types.includes(o.id)) - let array = [] - typeList.forEach(item => { - array = array.concat(item.familyMobileSignColumnList) - }) - // 根据 columnCode 去重 - const uniqueMap = new Map() - array.forEach(item => { - if (item.columnCode) { - uniqueMap.set(item.columnCode, item) - } - }) - // 可选:保留没有 columnCode 的项 - const withoutCode = array.filter(item => !item.columnCode) - this.typeArray = [...uniqueMap.values(), ...withoutCode] + // 唯一标识必须在每种类型中各出现一次且均为必填,不限定字段的验证规则。 + const firstColumns = typeList[0] ? (typeList[0].familyMobileSignColumnList || []) : [] + const options = firstColumns.filter(column => column.columnCode && typeList.length === types.length + && typeList.every(type => { + const matches = (type.familyMobileSignColumnList || []).filter(item => item.columnCode === column.columnCode) + return matches.length === 1 && matches[0].isRequired === true + })).map(column => ({ + columnCode: column.columnCode, + optionLabel: column.columnName, + legacy: false + })) + // 历史选择仍可回显,但不能作为新选择;不改写历史编码,也不静默清空原配置。 + if (this.formData.onlyKey && !options.some(item => item.columnCode === this.formData.onlyKey)) { + const oldColumn = typeList.flatMap(type => type.familyMobileSignColumnList || []) + .find(column => column.columnCode === this.formData.onlyKey) + options.push({ + columnCode: this.formData.onlyKey, + optionLabel: oldColumn ? oldColumn.columnName : '原字段', + legacy: true + }) + } + this.$set(this, 'typeArray', options) }, - async validNumber(row, old) { + // 老活动空配置按原来的按类型累计、不检查重叠回显,沿用活动时也保留所选规则。 + applyRuleDefaults() { + this.$set(this.formData, 'familyCountScope', Number(this.formData.familyCountScope) === 2 ? 2 : 1) + this.$set(this.formData, 'timeConflictCheck', this.formData.timeConflictCheck === true) + if (this.formData.courseList.some(course => course.courseType)) { + this.keyFocus() + } else { + this.$set(this, 'typeArray', []) + } + }, + validNumber(row, old) { if (GetQueryString("id") === "") { if (row.courseReservedNumber > row.coursePeopleNumber && row.reserveMode === 1) { this.$alert("预留人数不能大于" + this.activityType + "人数!", "提示", { @@ -402,31 +447,32 @@ const basicForm = { row.courseReservedNumber = 0 } } else { - const registerUserCount = await this.getRegisterUserCount(row.id) - if (row.courseReservedNumber + registerUserCount > row.coursePeopleNumber) { - if (row.reserveMode === 1) { - let num = row.coursePeopleNumber - registerUserCount - let str = - "您设置的" + - this.activityType + - "人数为" + - row.coursePeopleNumber + - "人,当前报名人数为" + - registerUserCount + - "人,预留名额上限为" + - num + - "人!" - this.$alert(str, "提示", { - confirmButtonText: "确定" - }) - row.courseReservedNumber = 0 + return this.getRegisterUserCount(row.id).then((registerUserCount) => { + if (row.courseReservedNumber + registerUserCount > row.coursePeopleNumber) { + if (row.reserveMode === 1) { + let num = row.coursePeopleNumber - registerUserCount + let str = + "您设置的" + + this.activityType + + "人数为" + + row.coursePeopleNumber + + "人,当前报名人数为" + + registerUserCount + + "人,预留名额上限为" + + num + + "人!" + this.$alert(str, "提示", { + confirmButtonText: "确定" + }) + row.courseReservedNumber = 0 + } } - } + }) } }, - async getRegisterUserCount(courseId) { - const resp = await this.$axios.post("/platform/family/manage/getRegisterUserCount", { courseId }) - return resp.code === 0 ? resp.data : 0 + getRegisterUserCount(courseId) { + return this.$axios.post("/platform/family/manage/getRegisterUserCount", { courseId }) + .then((resp) => resp.code === 0 ? resp.data : 0) }, courseTypeChange(val, row) { row.reserveMode = 1 @@ -497,30 +543,32 @@ const basicForm = { addCourse() { this.formData.courseList.push({ courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, courseIsLimitApply: false }) }, - async historicalActChange(val) { - const resp = await this.$axios.post("/platform/family/manage/findOne", {id: val}) - if (resp.code === 0) { - this.formData = resp.data - this.typeChange(this.formData.trainType) - this.formData.id = "" - this.formData.courseList.forEach((course) => { - course.id = "" - course.activityId = "" - if (course.courseTimeList) { - course.courseTimeList.forEach((courseTime) => { - courseTime.id = "" - courseTime.activityId = "" - courseTime.courseId = "" + historicalActChange(val) { + return this.$axios.post("/platform/family/manage/findOne", {id: val}).then((resp) => { + if (resp.code === 0) { + this.$set(this, "formData", resp.data) + this.applyRuleDefaults() + this.typeChange(this.formData.trainType) + this.formData.id = "" + this.formData.courseList.forEach((course) => { + course.id = "" + course.activityId = "" + if (course.courseTimeList) { + course.courseTimeList.forEach((courseTime) => { + courseTime.id = "" + courseTime.activityId = "" + courseTime.courseId = "" + }) + } + }) + if (this.formData.typeLimits) { + this.formData.typeLimits.forEach((typeLimit) => { + typeLimit.id = "" + typeLimit.activityId = "" }) } - }) - if (this.formData.typeLimits) { - this.formData.typeLimits.forEach((typeLimit) => { - typeLimit.id = "" - typeLimit.activityId = "" - }) } - } + }) }, typeChange(val) { const type = this.dict.type.FAMILY_SIGNUP_TYPE.find(o => o.code === val) @@ -534,15 +582,13 @@ const basicForm = { this.$set(this.formData, "activityGroupName", "全部教职工") } }, - async getActivityGroup() { - const { data } = await this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup") - return data + getActivityGroup() { + return this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup").then((resp) => resp.data) }, - async getHistoricalActList() { - const resp = await this.$axios.post("/platform/family/manage/getHistoricalActList", {}) - return resp.data + getHistoricalActList() { + return this.$axios.post("/platform/family/manage/getHistoricalActList", {}).then((resp) => resp.data) }, - async onSave() { + onSave() { let valid = false this.$refs.form.validateField("activityName", (errMsg) => { valid = errMsg === "" @@ -551,10 +597,10 @@ const basicForm = { this.$message.warning("请输入活动名称") return } - await this.doHandle('保存') + return this.doHandle('保存') }, onSubmit() { - this.$refs["form"].validate(async (valid, errMsg) => { + this.$refs["form"].validate((valid, errMsg) => { if (valid) { const courseValid = this.formData.courseList.some((v, i) => { const basicValid = v.courseName && v.coursePeopleNumber && v.courseLocation && v.courseInstructor && v.courseType @@ -599,7 +645,7 @@ const basicForm = { return } this.formData.isDisabled = false - await this.doHandle('提交') + this.doHandle('提交') } else { if(Object.keys(errMsg).length > 0) { this.$message.warning(errMsg[Object.keys(errMsg)[0]][0].message) @@ -609,7 +655,7 @@ const basicForm = { } }) }, - async doHandle(type) { + doHandle(type) { const cloneData = clone(this.formData) cloneData.activitySignUpStartTime = cloneData.activitySignTime !== undefined ? cloneData.activitySignTime[0] : null cloneData.activitySignUpEndTime = cloneData.activitySignTime !== undefined ? cloneData.activitySignTime[1] : null @@ -620,41 +666,50 @@ const basicForm = { } cloneData.typeLimits = JSON.stringify(this.formData.typeLimits) cloneData.courseList = JSON.stringify(cloneData.courseList) - const confirm = await this.$confirm("您确定要" + type + "吗?", "提示", { + return this.$confirm("您确定要" + type + "吗?", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" - }) - if (confirm !== "confirm") return - const resp = await this.$axios.post("/platform/family/manage/doHandle", cloneData) - if (resp.code === 0) { - this.$message.success(resp.msg) - this.step = 1 - this.$emit('refresh') - this.$emit('back') - } else { - this.$message.warning(resp.msg) + }).then(() => { + this.formLoading = true + return this.$axios.post("/platform/family/manage/doHandle", cloneData).then((resp) => { + if (resp.code === 0) { + this.$message.success(resp.msg) + this.step = 1 + this.$emit('refresh') + this.$emit('back') + } else { + this.$message.warning(resp.msg) + } + }).finally(() => { + this.formLoading = false + }) + }).catch(() => {}) + }, + getAllType() { + return this.$axios.post("/platform/family/type/getAllType", {}).then((resp) => resp.data) + }, + init(row) { + if (!row || !row.id) { + this.applyRuleDefaults() + return Promise.resolve() } - }, - async getAllType() { - const resp = await this.$axios.post("/platform/family/type/getAllType", {}) - return resp.data - }, - async init(row) { - if (row && row.id) { - const resp = await this.$axios.post("/platform/family/manage/findOne", {id: row.id}) + return this.$axios.post("/platform/family/manage/findOne", {id: row.id}).then((resp) => { if (resp.code === 0) { - this.formData = resp.data + this.$set(this, "formData", resp.data) this.typeChange(this.formData.trainType) - if(this.formData.onlyKey) this.keyFocus() + this.applyRuleDefaults() } - } + }) }, - async initData(row) { - this.activityGroupList = await this.getActivityGroup() - this.historicalActList = await this.getHistoricalActList() - this.courseTypeList = await this.getAllType() - await this.init(row) + initData(row) { + return Promise.all([this.getActivityGroup(), this.getHistoricalActList(), this.getAllType()]) + .then(([groups, activities, types]) => { + this.$set(this, "activityGroupList", groups) + this.$set(this, "historicalActList", activities) + this.$set(this, "courseTypeList", types) + return this.init(row) + }) }, }, watch: { @@ -664,9 +719,11 @@ const basicForm = { } }, 'formData.activityGroupId': { - async handler(newVal, oldVal) { - this.activityGroupList = await this.getActivityGroup() - this.userScopeDialog = false + handler(newVal, oldVal) { + this.getActivityGroup().then((groups) => { + this.$set(this, 'activityGroupList', groups) + this.$set(this, 'userScopeDialog', false) + }) }, deep: true }, diff --git a/src/main/resources/views/platform/zhgh/activity/family/manage/info.js b/src/main/resources/views/platform/zhgh/activity/family/manage/info.js index 98a4420..6ff6385 100644 --- a/src/main/resources/views/platform/zhgh/activity/family/manage/info.js +++ b/src/main/resources/views/platform/zhgh/activity/family/manage/info.js @@ -11,6 +11,10 @@ const info = { {{viewData.activityEndTime}} {{viewData.activitySignUpStartTime}} {{viewData.activitySignUpEndTime}} + {{viewData.familyCountScope === 2 ? '按整个活动累计' : '按子活动类型累计'}} + {{viewData.familyMaxCount}} + {{viewData.timeConflictCheck === true ? '是(含候补)' : '否'}} + {{viewData.onlyKey}}
@@ -54,6 +58,7 @@ const info = {
+ `, dicts: ["FAMILY_SIGNUP_TYPE"], @@ -72,7 +77,7 @@ const info = { this.$axios.post("/platform/family/manage/findOne", { id: id }) .then((resp) => { if (resp.code === 0) { - this.viewData = resp.data + this.$set(this, 'viewData', resp.data) const type = this.dict.type.FAMILY_SIGNUP_TYPE.find((o) => o.code === resp.data.trainType) this.activityType = type?.name || "子活动" } diff --git a/src/main/resources/views/platform/zhghh5/activity/family/list/applyForm.js b/src/main/resources/views/platform/zhghh5/activity/family/list/applyForm.js index 9eb4c9c..f13a86c 100644 --- a/src/main/resources/views/platform/zhghh5/activity/family/list/applyForm.js +++ b/src/main/resources/views/platform/zhghh5/activity/family/list/applyForm.js @@ -52,13 +52,16 @@ const applyForm = { +
+ 本活动累计最多报名{{activity.familyMaxCount}}名不同{{activity.keyWord}},含候补;多个子活动请使用已报名的家属。 +
- +
@@ -111,6 +114,12 @@ const applyForm = { this.$toast(this.activity.keyWord + '最多人数为' + this.courseType.familyMaxCount) return } + // 与PC一致,单次添加同时受类型和活动上限约束,跨课程累计由后端负责。 + if (this.activity.familyCountScope === 2 + && this.formData.mobileColumnsValue.length >= Number(this.activity.familyMaxCount)) { + this.$toast('本活动累计家属人数最多为' + this.activity.familyMaxCount) + return + } const list = clone(this.courseType.familyMobileSignColumnList) this.formData.mobileColumnsValue.push(list) },