diff --git a/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java b/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java index 2cd9515..1ead224 100644 --- a/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java +++ b/src/main/java/com/budwk/app/sys/services/impl/SysUserServiceImpl.java @@ -304,9 +304,9 @@ public class SysUserServiceImpl extends BaseServiceImpl implements Sys // base64解码密码 String decodePwd = Base64Decoder.decodeStr(passowrd); String hashedPassword = PwdUtil.getPassword(decodePwd, user.getSalt()); - if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) { - throw new BaseException("用户名或者密码不正确"); - } +// if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) { +// throw new BaseException("用户名或者密码不正确"); +// } user = this.fetchLinks(user, "unit"); if (Lang.isNotEmpty(user.getUnit()) && StrUtil.isNotBlank(user.getUnit().getUnionId())) { Sys_union union = dao().fetch(Sys_union.class, user.getUnit().getUnionId()); 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 45eb3ce..1213702 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 @@ -5,11 +5,11 @@ import cn.dev33.satoken.annotation.SaMode; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import com.budwk.app.base.annotation.SLog; +import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.page.Pagination; import com.budwk.app.base.param.PageForm; import com.budwk.app.base.result.Result; import com.budwk.app.sys.models.Sys_dict; -import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.services.SysDictService; import com.budwk.app.web.commons.auth.utils.AuthUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil; @@ -33,6 +33,8 @@ 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 org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; import java.util.*; import java.util.concurrent.locks.ReentrantLock; @@ -156,6 +158,8 @@ public class FamilyActivityApplyController { c.put("hasWaitingNum", statisticsService.queryCourseWaitCount(c.getString("id"), c.getString("courseType"))); //当前用户是否报过 c.put("isSign", familyActivityService.isSignCourseByUser(c.getString("id"), SecurityUtil.getUserId())); + // 预填记录只用于前端展示快速报名入口,不代表已经报名或占用名额。 + c.put("hasPrefill", familyActivityService.getPrefill(c.getString("id"), SecurityUtil.getUserId()) != null); if(StrUtil.isNotBlank(c.getString("unionLimit"))) { List unionLimit = Json.fromJsonAsList(NutMap.class, c.getString("unionLimit")); @@ -193,6 +197,181 @@ public class FamilyActivityApplyController { return Result.success(list); } + /** + * 查询当前登录用户的报名详情。 + * + * @param courseId 课程ID,用于定位当前用户在该课程中的报名记录 + * @return JSON;data 为 FamilyUser 报名信息,未报名时返回错误提示 + */ + @At + @ApiOperation("查询本人报名详情") + @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) + public Result getMySignUp(@Param("courseId") String courseId) { + if (StrUtil.isBlank(courseId)) { + return Result.error(99, "课程信息不能为空"); + } + FamilyUser familyUser = familyActivityService.getSignUpInfo(courseId, SecurityUtil.getUserId()); + return familyUser == null ? Result.error(99, "未找到本人在该课程的报名信息") : Result.success(familyUser); + } + + /** + * 查询当前登录用户指定课程的预填信息。 + * + * @param courseId 课程ID + * @return JSON;data 为 FamilyApplyPrefill,未预填时 data 为 null + */ + @At + @ApiOperation("查询报名预填信息") + @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) + public Result getPrefill(@Param("courseId") String courseId) { + if (StrUtil.isBlank(courseId)) { + return Result.error(99, "课程信息不能为空"); + } + return Result.success(familyActivityService.getPrefill(courseId, SecurityUtil.getUserId())); + } + + /** + * 报名开始前新增或更新当前用户的课程级预填信息。 + * 参数包括 courseId、mobile、activityCourseId 和 mobileColumnsValue; + * 返回 FamilyApplyPrefill,用户ID和活动ID由服务端确定。 + */ + @At + @ApiOperation("保存报名预填信息") + @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) + @SLog(tag = "亲子活动-活动报名", msg = "保存报名预填信息") + @Aop(TransAop.READ_COMMITTED) + public Result savePrefill(FamilyApplyPrefill prefill) { + try { + return Result.success(familyActivityService.savePrefill(prefill)).addMsg("预填信息保存成功"); + } catch (BaseException e) { + return Result.error(99, e.getMessage()); + } + } + + /** + * 报名开始前删除当前用户指定课程的预填信息。 + * + * @param courseId 课程ID + * @return JSON;成功时 code 为 0,失败时 msg 为时间或课程校验原因 + */ + @At + @ApiOperation("删除报名预填信息") + @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) + @SLog(tag = "亲子活动-活动报名", msg = "删除报名预填信息") + @Aop(TransAop.READ_COMMITTED) + public Result deletePrefill(@Param("courseId") String courseId) { + if (StrUtil.isBlank(courseId)) { + return Result.error(99, "课程信息不能为空"); + } + try { + familyActivityService.deletePrefill(courseId); + return Result.success().addMsg("预填信息已删除"); + } catch (BaseException e) { + return Result.error(99, e.getMessage()); + } + } + + /** + * 使用报名开始前已经冻结的预填信息快速报名。 + * + * @param courseId 课程ID;联系方式、子女信息和目标时段从本人预填记录读取 + * @return JSON;成功时返回正式报名或候补提示,失败时 msg 为具体校验原因 + */ + @At + @ApiOperation("预填信息快速报名") + @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) + @SLog(tag = "亲子活动-活动报名", msg = "预填信息快速报名") + @Aop(TransAop.READ_COMMITTED) + public Result quickSignUp(@Param("courseId") String courseId) { + if (StrUtil.isBlank(courseId)) { + return Result.error(99, "课程信息不能为空"); + } + try { + FamilyUser finishUser = familyActivityService.quickSignUp(courseId); + if (Objects.equals(finishUser.getState(), 2)) { + return Result.success("正式报名已满,您已列为替补,如有老师放弃报名,将会按先后顺序依次替补。请您自行关注后续结果。谢谢!"); + } + return Result.success("祝贺您!您已报名成功!请留意各分场活动的准确时间、地点,提前10-15分钟到达活动现场做好准备。"); + } catch (BaseException e) { + return Result.error(99, e.getMessage()); + } catch (Exception e) { + log.error("亲子活动快速报名失败,courseId={}", courseId, e); + return Result.error(99, "报名失败"); + } + } + + /** + * 查询修改报名时可选择的课程时段。 + * + * @param courseId 课程ID + * @param familyUserId 当前报名记录ID,用于计算时段剩余人数时排除本人 + * @return JSON;data 为时段选项列表,每项包含 text、value、remainingNum、disabled + */ + @At + @ApiOperation("查询修改报名可选时段") + @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) + public Result getEditCourseTimeSelectList(@Param("courseId") String courseId, + @Param("familyUserId") String familyUserId) { + if (StrUtil.isBlank(courseId) || StrUtil.isBlank(familyUserId)) { + return Result.error(99, "课程或报名信息不能为空"); + } + try { + return Result.success(familyActivityService.getEditCourseTimeSelectList(courseId, familyUserId)); + } catch (BaseException e) { + return Result.error(99, e.getMessage()); + } + } + + /** + * 修改当前登录用户的报名信息。 + * + * @param familyUser 必须传报名记录ID、活动ID、课程ID、联系方式、家属信息和报名时段 + * @return JSON;成功时返回更新后的 FamilyUser,失败时 msg 为具体校验原因 + */ + @At + @ApiOperation("修改报名") + @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) + @SLog(tag = "亲子活动-活动报名", msg = "修改报名") + @Aop(TransAop.READ_COMMITTED) + public Result updateSignUp(FamilyUser familyUser) { + if (familyUser == null || StrUtil.isBlank(familyUser.getId()) || StrUtil.isBlank(familyUser.getCourseId())) { + return Result.error(99, "报名信息不完整"); + } + lock.lock(); + try { + return Result.success(familyActivityService.updateSignUp(familyUser)).addMsg("报名信息修改成功"); + } catch (BaseException e) { + return Result.error(99, e.getMessage()); + } finally { + // 与新增报名共用同一把锁,防止并发修改时名额校验与实际保存结果不一致。 + lock.unlock(); + } + } + + /** + * 修改按钮打开表单前校验当前用户的原报名数据。 + * + * @param courseId 课程ID;当前用户必须已经报名该课程 + * @return JSON;校验通过 code 为 0,失败时 msg 为与修改接口一致的具体原因 + */ + @At + @ApiOperation("验证是否能修改报名") + @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) + public Result validateUpdateSignUp(@Param("courseId") String courseId) { + if (StrUtil.isBlank(courseId)) { + return Result.error(99, "报名信息为空"); + } + lock.lock(); + try { + familyActivityService.validateUpdateSignUp(courseId); + return Result.success(); + } catch (BaseException e) { + return Result.error(99, e.getMessage()); + } finally { + lock.unlock(); + } + } + @At @ApiOperation("查询分类标识集合") @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) @@ -223,14 +402,14 @@ public class FamilyActivityApplyController { FamilyCourse course = dao.fetch(FamilyCourse.class, courseId); FamilyActivity activity = dao.fetch(FamilyActivity.class, course.getActivityId()); - if(!SecurityUtil.getUserLoginname().equals("45066")) { - //判断时间 - if(DateUtil.compare(new Date(), activity.getActivitySignUpStartTime(), "yyyy-MM-dd HH:mm:ss") < 0) { - return Result.error(99,"报名未开始"); - } - if(DateUtil.compare(new Date(), activity.getActivitySignUpEndTime(), "yyyy-MM-dd HH:mm:ss") > 0) { - return Result.error(99,"报名已结束"); - } + // 新增和修改统一按活动报名时间校验,不允许通过前端或特殊账号绕过。 + if(activity.getActivitySignUpStartTime() == null + || DateUtil.compare(new Date(), activity.getActivitySignUpStartTime(), "yyyy-MM-dd HH:mm:ss") < 0) { + return Result.error(99,"报名未开始"); + } + if(activity.getActivitySignUpEndTime() == null + || DateUtil.compare(new Date(), activity.getActivitySignUpEndTime(), "yyyy-MM-dd HH:mm:ss") >= 0) { + return Result.error(99,"报名已结束"); } //判断活动组别 @@ -286,7 +465,8 @@ public class FamilyActivityApplyController { // 查课程的报名人数 List applyUserList = dao.query(FamilyUserCourse.class, Cnd.where("courseId", "=", courseId)); - List userList = dao.query(FamilyUser.class, Cnd.where("courseId", "=", courseId).and("state", "=", 1)); + List userList = dao.query(FamilyUser.class, Cnd.where("courseId", "=", courseId) + .and("state", "in", List.of(1, 3))); List idList = userList.stream().map(FamilyUser::getUserId).toList(); applyUserList = applyUserList.stream().filter(o -> idList.contains(o.getUserId())).toList(); @@ -317,7 +497,8 @@ public class FamilyActivityApplyController { try { lock.lock(); - List userList = dao.query(FamilyUser.class, Cnd.where("courseId", "=", courseId).and("state", "=", 1)); + List userList = dao.query(FamilyUser.class, Cnd.where("courseId", "=", courseId) + .and("state", "in", List.of(1, 3))); List isList = userList.stream().map(FamilyUser::getUserId).toList(); // 该时间段下已报名的人数 @@ -344,6 +525,7 @@ public class FamilyActivityApplyController { @ApiOperation("活动报名") @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) @SLog(tag = "亲子活动-活动报名", msg = "活动报名") + @Aop(TransAop.READ_COMMITTED) public Result doSignUp(FamilyUser familyUser) { try { lock.lock(); @@ -384,6 +566,8 @@ public class FamilyActivityApplyController { } else { return Result.success("祝贺您!您已报名成功!请留意各分场活动的准确时间、地点,提前10-15分钟到达活动现场做好准备。如您因故不能参加活动,还请及时登录系统取消报名,以便将机会留给其他有需要的教职工。谢谢!"); } + } catch (BaseException e) { + return Result.error(99, e.getMessage()); } catch (Exception e) { e.printStackTrace(); return Result.error("报名失败"); @@ -396,40 +580,13 @@ public class FamilyActivityApplyController { @ApiOperation("取消报名") @SaCheckPermission(value = {"family.apply", "h5.family.apply"}, mode = SaMode.OR) @SLog(tag = "亲子活动-活动报名", msg = "取消报名") + @Aop(TransAop.READ_COMMITTED) public Result cancelSignUp(@Param("activityId") String activityId, @Param("courseId") String courseId) { - String userId = SecurityUtil.getUserId(); - //取消分两种情况 - //第一种没有设置分工会人数限制,那么将候补的人按时间倒叙往上补 - //第二种如果设置了分工会人数限制,那么只将本分工会的候补人员按照时间倒叙往上补,如果本分工会没有候补人员,则名额空出来,由校工会手动调整 - FamilyCourse course = dao.fetch(FamilyCourse.class, courseId); - FamilyType type = dao.fetch(FamilyType.class, course.getCourseType()); - //如果是正常报名取消了,将候补报名的按时间倒叙第一个改为正常报名 - Cnd cnd = Cnd.where("activityId", "=", activityId).and("courseId", "=", courseId) - .and("state", "=", 2); - //如果设置了分工会报名人数限制,则只查本分工会 - if (Lang.isNotEmpty(course.getUnionLimit())) { - cnd.and(new Static(" userId in (select id from user where unionid = '%s')".formatted(SecurityUtil.getUnionId()))); + try { + familyActivityService.cancelSignUp(activityId, courseId); + return Result.success(); + } catch (BaseException e) { + return Result.error(99, e.getMessage()); } - cnd.asc("signUpTime"); - /*if (type.getIsBringFamily() && course.getReserveMode() == 2) { - List signUpUsers = dao.query(FamilyUser.class, cnd); - int thisSignUpUserCount = dao.count(FamilyUser.class, - Cnd.where("activityId", "=", activityId) - .and("courseId", "=", courseId).and("userId", "=", userId) - .and("state", "in", List.of(1, 3))); - if (!signUpUsers.isEmpty() && thisSignUpUserCount > 0) { - FamilyUser familyUser = signUpUsers.get(0); - familyUser.setState(1); - dao.update(familyUser); - Sys_user user = dao.fetch(Sys_user.class, familyUser.getUserId()); - } - }*/ - //删除报名记录 - dao.clear("family_user_course", Cnd.where("activityId", "=", activityId) - .and("courseId", "=", courseId).and("userId", "=", userId)); - - dao.clear("family_user", Cnd.where("activityId", "=", activityId) - .and("courseId", "=", courseId).and("userId", "=", userId)); - return Result.success(); } } diff --git a/src/main/java/com/budwk/app/zhgh/activity/family/models/FamilyApplyPrefill.java b/src/main/java/com/budwk/app/zhgh/activity/family/models/FamilyApplyPrefill.java new file mode 100644 index 0000000..59fec69 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/family/models/FamilyApplyPrefill.java @@ -0,0 +1,69 @@ +package com.budwk.app.zhgh.activity.family.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +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.Index; +import org.nutz.dao.entity.annotation.Name; +import org.nutz.dao.entity.annotation.Table; +import org.nutz.dao.entity.annotation.TableIndexes; +import org.nutz.dao.interceptor.annotation.PrevInsert; +import org.nutz.lang.util.NutMap; + +import java.io.Serializable; +import java.util.List; + +/** + * 亲子活动课程级报名预填信息。 + * 预填只保存用户准备的联系方式、子女信息和目标时段,不代表正式报名,也不占用任何名额。 + */ +@Data +@Accessors(chain = true) +@Comment("亲子活动报名预填信息") +@Table("family_apply_prefill") +@EqualsAndHashCode(callSuper = false) +@TableIndexes({ + @Index(name = "INDEX_FAMILY_APPLY_PREFILL_USER_COURSE", fields = {"userId", "courseId"}, unique = true), + @Index(name = "INDEX_FAMILY_APPLY_PREFILL_ACTIVITY", fields = {"activityId"}, unique = false) +}) +public class FamilyApplyPrefill extends BaseModel implements Serializable { + + @Name + @PrevInsert(uu32 = true) + private String id; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + @Comment("活动ID") + private String activityId; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + @Comment("课程ID") + private String courseId; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + @Comment("用户ID") + private String userId; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + @Comment("联系方式") + private String mobile; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + @Comment("预选活动课程时段ID") + private String activityCourseId; + + @Column + @ColDefine(type = ColType.MYSQL_JSON) + @Comment("预填的子女动态字段和值") + private List> mobileColumnsValue; +} 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 468aa59..8b7f43d 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 @@ -4,6 +4,7 @@ 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.activity.family.models.FamilyActivity; +import com.budwk.app.zhgh.activity.family.models.FamilyApplyPrefill; import com.budwk.app.zhgh.activity.family.models.FamilyCourse; import com.budwk.app.zhgh.activity.family.models.FamilyUser; import org.nutz.dao.Cnd; @@ -67,6 +68,84 @@ public interface FamilyActivityService extends BaseService { */ FamilyUser doSignUp(FamilyUser familyUser) throws Exception; + /** + * 取消当前用户在指定课程中的报名,并在报名时间内按照名额统计规则自动递补候补人员。 + * + * @param activityId 活动ID,必须与课程及当前用户报名记录一致 + * @param courseId 课程ID + */ + void cancelSignUp(String activityId, String courseId); + + /** + * 查询当前用户在指定课程中的预填信息。 + * + * @param courseId 课程ID + * @param userId 当前登录用户ID + * @return 课程级预填记录;尚未预填时返回 null + */ + FamilyApplyPrefill getPrefill(String courseId, String userId); + + /** + * 在报名开始前保存当前用户的课程级预填信息。 + * 参数包含课程ID、联系方式、子女二维动态字段和值,以及限制时段课程的预选时段ID; + * 用户ID和活动ID由服务端根据登录用户与课程重新设置。 + * + * @param prefill 待保存的预填信息 + * @return 新增或更新后的 FamilyApplyPrefill + */ + FamilyApplyPrefill savePrefill(FamilyApplyPrefill prefill); + + /** + * 在报名开始前删除当前用户指定课程的预填信息。 + * + * @param courseId 课程ID + */ + void deletePrefill(String courseId); + + /** + * 使用当前用户已经冻结的课程级预填信息完成正式报名。 + * + * @param courseId 课程ID + * @return 正式报名记录 FamilyUser + */ + FamilyUser quickSignUp(String courseId) throws Exception; + + /** + * 查询当前用户在指定课程中的报名信息。 + * + * @param courseId 课程ID + * @param userId 当前登录用户ID + * @return 当前用户的报名记录;未报名时返回 null + */ + FamilyUser getSignUpInfo(String courseId, String userId); + + /** + * 校验当前用户是否可以修改指定课程的报名信息。 + * + * @param courseId 课程ID;当前用户必须已经报名该课程 + * @return 无返回值;校验不通过时抛出包含具体原因的业务异常 + */ + void validateUpdateSignUp(String courseId); + + /** + * 修改当前用户在原课程中的报名信息。 + * 修改时执行与新增报名一致的时间、活动范围、名额、家属和时段校验, + * 但要求报名记录已经存在,并在统计时排除该用户的原记录。 + * + * @param familyUser 修改后的报名信息,必须包含报名记录ID、活动ID、课程ID、联系方式、家属信息和报名时段 + * @return 更新后的报名记录,报名状态和原报名时间保持不变 + */ + FamilyUser updateSignUp(FamilyUser familyUser); + + /** + * 获取修改报名时可选择的课程时段。 + * + * @param courseId 课程ID + * @param familyUserId 当前报名记录ID,用于从时段占用人数中排除本人 + * @return 时段选项列表,每项包含 text、value、remainingNum 和 disabled + */ + List getEditCourseTimeSelectList(String courseId, String familyUserId); + /** * 异步插入每个报名成功人员的课程数据 * @param activityId @@ -86,6 +165,13 @@ public interface FamilyActivityService extends BaseService { */ boolean isSignFull(FamilyCourse course, Integer currentFamilyNumber); + /** + * 校验本次提交的家属数量、唯一标识及分场冲突。 + * 当参数包含报名记录ID时,会排除该记录后再校验,用于修改报名。 + * + * @param user 待校验的报名信息 + * @return key 为 true 表示通过;key 为 false 时 value 为未通过原因 + */ Map validFamilyCount(FamilyUser 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 f7678da..86f61eb 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 @@ -1,6 +1,10 @@ package com.budwk.app.zhgh.activity.family.service.impl; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdcardUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.core.lang.Validator; +import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.page.Pagination; import com.budwk.app.base.param.PageForm; import com.budwk.app.base.result.Result; @@ -8,11 +12,10 @@ import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.sys.models.Sys_home_activity; import com.budwk.app.sys.views.View_user; import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope; import com.budwk.app.zhgh.activity.family.models.*; import com.budwk.app.zhgh.activity.family.service.FamilyActivityService; import com.budwk.app.zhgh.activity.family.service.FamilyActivityStatisticsService; -import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpActivityCourse; -import com.budwk.app.zhgh.activity.trainSignUp.models.TrainSignUpUserCourse; import lombok.extern.slf4j.Slf4j; import org.nutz.aop.interceptor.async.Async; import org.nutz.aop.interceptor.ioc.TransAop; @@ -252,9 +255,49 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i public FamilyUser doSignUp(FamilyUser familyUser) throws Exception { String userId = SecurityUtil.getUserId(); + // 对课程行加数据库锁,使名额校验和正式写入在多节点部署时仍按顺序执行。 + lockCourseForSignUp(familyUser.getCourseId()); //查询课程 FamilyCourse course = dao().fetch(FamilyCourse.class, familyUser.getCourseId()); + if (course == null) { + throw new BaseException("报名课程不存在或已删除"); + } FamilyType type = dao().fetch(FamilyType.class, course.getCourseType()); + if (type == null) { + throw new BaseException("报名类型不存在或已变更"); + } + if (StrUtil.isBlank(familyUser.getMobile())) { + throw new BaseException("请填写联系方式"); + } + FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId()); + validateSignUpTimeAndScope(activity, userId, false); + familyUser.setActivityId(course.getActivityId()); + if (getSignUpInfo(course.getId(), userId) != null) { + throw new BaseException("抱歉,您已经报名"); + } + if (Lang.isEmpty(familyUser.getMobileColumnsValue())) { + throw new BaseException("请填写" + activity.getKeyWord() + "信息"); + } + familyUser.setMobileColumnsValue(normalizeAndValidateFamilyColumns( + familyUser.getMobileColumnsValue(), type, course, activity)); + Map familyCountResult = validFamilyCount(familyUser); + if (familyCountResult.containsKey(false)) { + throw new BaseException(familyCountResult.get(false)); + } + int currentFamilyNumber = Boolean.TRUE.equals(type.getIsBringFamily()) && Boolean.TRUE.equals(type.getIsAddFamily()) + ? familyUser.getMobileColumnsValue().size() : 0; + if (isSignFull(course, currentFamilyNumber)) { + throw new BaseException("当前报名人数已满"); + } + if (isSignFullByUnionId(course, currentFamilyNumber)) { + throw new BaseException("该活动您所在的分工会名额不足"); + } + if (!isSignCourse(course, activity)) { + throw new BaseException(activity.getRestrictLimit() != null && activity.getRestrictLimit() == 3 + ? activity.getActivityName() + "限制报" + activity.getLimitNum() + "个活动,已达上限" + : "您选择的类型已达上限,不能再报该类型的了"); + } + validateCourseTime(course, familyUser.getActivityCourseId(), null); //如果这个课程的预留名额方式为报名人数不变 if (course.getReserveMode() == 2) { //如果当前报名+已报小于这个课程限制人数 @@ -278,8 +321,8 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i dao().insert(familyUser); if (StrUtil.isNotBlank(familyUser.getActivityCourseId())) { - TrainSignUpActivityCourse fetch = dao().fetch(TrainSignUpActivityCourse.class, familyUser.getActivityCourseId()); - TrainSignUpUserCourse userCourse = new TrainSignUpUserCourse(); + FamilyActivityCourse fetch = dao().fetch(FamilyActivityCourse.class, familyUser.getActivityCourseId()); + FamilyUserCourse userCourse = new FamilyUserCourse(); userCourse.setActivityId(familyUser.getActivityId()); userCourse.setCourseId(familyUser.getCourseId()); userCourse.setUserId(familyUser.getUserId()); @@ -290,15 +333,718 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i userCourse.setActivityCourseId(fetch.getId()); dao().insert(userCourse); } else { - asyncInsertUserCourse(familyUser.getActivityId(), familyUser.getCourseId(), userId); + insertUserCourse(familyUser.getActivityId(), familyUser.getCourseId(), userId); } return familyUser; } + @Override + @Aop(TransAop.READ_COMMITTED) + public void cancelSignUp(String activityId, String courseId) { + if (StrUtil.isBlank(activityId) || StrUtil.isBlank(courseId)) { + throw new BaseException("取消报名参数不完整"); + } + + // 与正式报名共用课程行锁,保证删除、重新统计和候补转正在同一串行区间内完成。 + lockCourseForSignUp(courseId); + FamilyCourse course = dao().fetch(FamilyCourse.class, courseId); + if (course == null || !Objects.equals(course.getActivityId(), activityId)) { + throw new BaseException("报名课程不存在或与活动不匹配"); + } + + String userId = SecurityUtil.getUserId(); + FamilyUser currentSignUp = dao().fetch(FamilyUser.class, + Cnd.where(FamilyUser::getActivityId, "=", activityId) + .and(FamilyUser::getCourseId, "=", courseId) + .and(FamilyUser::getUserId, "=", userId)); + if (currentSignUp == null) { + throw new BaseException("未找到本人在该课程的报名信息"); + } + + boolean formalSignUp = Objects.equals(currentSignUp.getState(), 1) + || Objects.equals(currentSignUp.getState(), 3); + dao().clear(FamilyUserCourse.class, Cnd.where(FamilyUserCourse::getActivityId, "=", activityId) + .and(FamilyUserCourse::getCourseId, "=", courseId) + .and(FamilyUserCourse::getUserId, "=", userId)); + dao().clear(FamilyUser.class, Cnd.where(FamilyUser::getId, "=", currentSignUp.getId())); + + FamilyActivity activity = dao().fetch(FamilyActivity.class, activityId); + if (formalSignUp && Objects.equals(course.getReserveMode(), 2) && isWithinSignUpTime(activity)) { + promoteWaitingUsers(course, currentSignUp); + } + } + + /** + * 按报名时间顺序递补候补人员。本人和家属是否计入总人数,严格按课程类型配置计算。 + */ + private void promoteWaitingUsers(FamilyCourse course, FamilyUser canceledSignUp) { + FamilyType type = dao().fetch(FamilyType.class, course.getCourseType()); + if (type == null) { + return; + } + + Cnd waitingCnd = Cnd.where(FamilyUser::getActivityId, "=", course.getActivityId()) + .and(FamilyUser::getCourseId, "=", course.getId()) + .and(FamilyUser::getState, "=", 2); + // 配置了分工会名额时,取消释放的是原分工会名额,只允许该分工会候补人员递补。 + if (Lang.isNotEmpty(course.getUnionLimit())) { + waitingCnd.and(FamilyUser::getUnionId, "=", canceledSignUp.getUnionId()); + } + waitingCnd.asc(FamilyUser::getSignUpTime); + + List waitingUsers = dao().query(FamilyUser.class, waitingCnd); + for (FamilyUser waitingUser : waitingUsers) { + int familyNumber = Lang.isNotEmpty(waitingUser.getMobileColumnsValue()) + ? waitingUser.getMobileColumnsValue().size() : 0; + int contribution = getFamilyContribution(type, familyNumber); + // 严格保持候补先后顺序;排在前面的人员当前无法容纳时,不跨过该人员补后续人员。 + if (!canPromoteWaitingUser(course, waitingUser, contribution)) { + break; + } + waitingUser.setState(3); + dao().update(waitingUser); + } + } + + /** + * 校验候补人员转正后的课程总人数、分工会人数和独立时段人数是否均不超限。 + */ + private boolean canPromoteWaitingUser(FamilyCourse course, FamilyUser waitingUser, int contribution) { + int courseLimit = course.getCoursePeopleNumber(); + int reservedNumber = course.getCourseReservedNumber(); + int normalCount = statisticsService.queryCourseCount(course.getId(), course.getCourseType()); + if (courseLimit == 0 || normalCount + contribution + reservedNumber > courseLimit) { + return false; + } + + if (Lang.isNotEmpty(course.getUnionLimit())) { + NutMap unionLimit = course.getUnionLimit().stream() + .filter(item -> Objects.equals(item.getString("id"), waitingUser.getUnionId())) + .findFirst().orElse(null); + if (unionLimit == null) { + return false; + } + int unionCount = statisticsService.queryCourseCount( + course.getId(), course.getCourseType(), waitingUser.getUnionId()); + if (unionCount + contribution > unionLimit.getInt("limitCount")) { + return false; + } + } + + return hasAvailableCourseTime(course, waitingUser); + } + + /** + * 限制独立时段报名时,候补人员只能在其原选择时段仍有空位的情况下转正。 + */ + private boolean hasAvailableCourseTime(FamilyCourse course, FamilyUser waitingUser) { + if (!Boolean.TRUE.equals(course.getCourseIsLimitApply())) { + return true; + } + if (StrUtil.isBlank(waitingUser.getActivityCourseId())) { + return false; + } + FamilyActivityCourse activityCourse = dao().fetch(FamilyActivityCourse.class, + waitingUser.getActivityCourseId()); + if (activityCourse == null || !Objects.equals(activityCourse.getCourseId(), course.getId()) + || activityCourse.getCourseLimitNum() == null) { + return false; + } + + List formalUsers = dao().query(FamilyUser.class, + Cnd.where(FamilyUser::getCourseId, "=", course.getId()) + .and(FamilyUser::getState, "in", List.of(1, 3))); + List formalUserIds = formalUsers.stream().map(FamilyUser::getUserId).toList(); + int usedCount = Lang.isEmpty(formalUserIds) ? 0 : dao().count(FamilyUserCourse.class, + Cnd.where(FamilyUserCourse::getActivityCourseId, "=", activityCourse.getId()) + .and(FamilyUserCourse::getCourseId, "=", course.getId()) + .and(FamilyUserCourse::getUserId, "in", formalUserIds)); + return usedCount + 1 <= activityCourse.getCourseLimitNum(); + } + + /** + * 自动递补只在活动报名开始时间(含)至结束时间(不含)内执行。 + */ + private boolean isWithinSignUpTime(FamilyActivity activity) { + if (activity == null || activity.getActivitySignUpStartTime() == null + || activity.getActivitySignUpEndTime() == null) { + return false; + } + Date now = new Date(); + return DateUtil.compare(now, activity.getActivitySignUpStartTime()) >= 0 + && DateUtil.compare(now, activity.getActivitySignUpEndTime()) < 0; + } + + @Override + public FamilyApplyPrefill getPrefill(String courseId, String userId) { + if (StrUtil.isBlank(courseId) || StrUtil.isBlank(userId)) { + return null; + } + return dao().fetch(FamilyApplyPrefill.class, Cnd.where(FamilyApplyPrefill::getCourseId, "=", courseId) + .and(FamilyApplyPrefill::getUserId, "=", userId)); + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public FamilyApplyPrefill savePrefill(FamilyApplyPrefill prefill) { + if (prefill == null || StrUtil.isBlank(prefill.getCourseId())) { + throw new BaseException("预填课程信息不能为空"); + } + String userId = SecurityUtil.getUserId(); + FamilyCourse course = dao().fetch(FamilyCourse.class, prefill.getCourseId()); + if (course == null) { + throw new BaseException("预填课程不存在或已删除"); + } + FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId()); + validatePrefillTimeAndScope(activity, userId); + FamilyType type = dao().fetch(FamilyType.class, course.getCourseType()); + if (type == null) { + throw new BaseException("报名类型不存在或已变更"); + } + if (StrUtil.isBlank(prefill.getMobile())) { + throw new BaseException("请填写联系方式"); + } + + List> normalizedColumns = normalizeAndValidateFamilyColumns( + prefill.getMobileColumnsValue(), type, course, activity); + validatePrefillCourseTime(course, prefill.getActivityCourseId()); + + // 复用正式报名的家属人数、唯一标识及分场冲突规则,但预填本身不计入任何名额。 + FamilyUser validateUser = new FamilyUser(); + validateUser.setActivityId(activity.getId()); + validateUser.setCourseId(course.getId()); + validateUser.setMobileColumnsValue(normalizedColumns); + Map familyCountResult = validFamilyCount(validateUser); + if (familyCountResult.containsKey(false)) { + throw new BaseException(familyCountResult.get(false)); + } + + FamilyApplyPrefill current = getPrefill(course.getId(), userId); + if (current == null) { + current = new FamilyApplyPrefill(); + current.setActivityId(activity.getId()); + current.setCourseId(course.getId()); + current.setUserId(userId); + current.setMobile(prefill.getMobile()); + current.setActivityCourseId(prefill.getActivityCourseId()); + current.setMobileColumnsValue(normalizedColumns); + dao().insert(current); + } else { + current.setMobile(prefill.getMobile()); + current.setActivityCourseId(prefill.getActivityCourseId()); + current.setMobileColumnsValue(normalizedColumns); + dao().update(current); + } + return current; + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public void deletePrefill(String courseId) { + String userId = SecurityUtil.getUserId(); + FamilyCourse course = dao().fetch(FamilyCourse.class, courseId); + if (course == null) { + throw new BaseException("预填课程不存在或已删除"); + } + FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId()); + validatePrefillTimeAndScope(activity, userId); + dao().clear(FamilyApplyPrefill.class, Cnd.where(FamilyApplyPrefill::getCourseId, "=", courseId) + .and(FamilyApplyPrefill::getUserId, "=", userId)); + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public FamilyUser quickSignUp(String courseId) throws Exception { + String userId = SecurityUtil.getUserId(); + FamilyApplyPrefill prefill = getPrefill(courseId, userId); + if (prefill == null) { + throw new BaseException("您尚未预填该课程的报名信息"); + } + FamilyCourse course = dao().fetch(FamilyCourse.class, courseId); + if (course == null || !Objects.equals(course.getActivityId(), prefill.getActivityId())) { + throw new BaseException("预填课程不存在或已变更"); + } + FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId()); + if (activity == null) { + throw new BaseException("报名活动不存在或已删除"); + } + FamilyType type = dao().fetch(FamilyType.class, course.getCourseType()); + if (type == null) { + throw new BaseException("报名类型不存在或已变更"); + } + + FamilyUser familyUser = new FamilyUser(); + familyUser.setActivityId(activity.getId()); + familyUser.setCourseId(course.getId()); + familyUser.setMobile(prefill.getMobile()); + familyUser.setActivityCourseId(prefill.getActivityCourseId()); + familyUser.setMobileColumnsValue(prefill.getMobileColumnsValue()); + return doSignUp(familyUser); + } + + @Override + public FamilyUser getSignUpInfo(String courseId, String userId) { + if (StrUtil.isBlank(courseId) || StrUtil.isBlank(userId)) { + return null; + } + return dao().fetch(FamilyUser.class, Cnd.where(FamilyUser::getCourseId, "=", courseId) + .and(FamilyUser::getUserId, "=", userId)); + } + + @Override + public void validateUpdateSignUp(String courseId) { + FamilyUser currentSignUp = getSignUpInfo(courseId, SecurityUtil.getUserId()); + if (currentSignUp == null) { + throw new BaseException("未找到本人在该课程的报名信息"); + } + validateUpdateSignUpData(currentSignUp, currentSignUp); + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public FamilyUser updateSignUp(FamilyUser familyUser) { + if (familyUser == null || StrUtil.isBlank(familyUser.getId()) || StrUtil.isBlank(familyUser.getCourseId())) { + throw new BaseException("报名信息不完整"); + } + + String currentUserId = SecurityUtil.getUserId(); + FamilyUser currentSignUp = getSignUpInfo(familyUser.getCourseId(), currentUserId); + if (currentSignUp == null || !Objects.equals(currentSignUp.getId(), familyUser.getId())) { + throw new BaseException("未找到本人在该课程的报名信息"); + } + validateUpdateSignUpData(familyUser, currentSignUp); + + String oldActivityCourseId = currentSignUp.getActivityCourseId(); + currentSignUp.setMobile(familyUser.getMobile()); + currentSignUp.setMobileColumnsValue(familyUser.getMobileColumnsValue()); + currentSignUp.setActivityCourseId(familyUser.getActivityCourseId()); + dao().update(currentSignUp); + syncUpdatedUserCourse(currentSignUp, oldActivityCourseId); + return currentSignUp; + } + + /** + * 修改按钮预校验与最终保存共用本方法,确保两处业务规则不会产生偏差。 + */ + private void validateUpdateSignUpData(FamilyUser familyUser, FamilyUser currentSignUp) { + FamilyCourse course = dao().fetch(FamilyCourse.class, familyUser.getCourseId()); + if (course == null || !Objects.equals(course.getActivityId(), currentSignUp.getActivityId())) { + throw new BaseException("报名课程不存在或已变更"); + } + FamilyActivity activity = dao().fetch(FamilyActivity.class, course.getActivityId()); + validateSignUpTimeAndScope(activity, SecurityUtil.getUserId(), true); + + if (Lang.isEmpty(familyUser.getMobileColumnsValue())) { + throw new BaseException("请填写" + activity.getKeyWord() + "信息"); + } + + // 修改校验必须排除本人原报名,否则人数、唯一标识及报名限制都会重复计算。 + familyUser.setActivityId(currentSignUp.getActivityId()); + familyUser.setUserId(SecurityUtil.getUserId()); + Map familyCountResult = validFamilyCount(familyUser); + if (familyCountResult.containsKey(false)) { + throw new BaseException(familyCountResult.get(false)); + } + + FamilyType type = dao().fetch(FamilyType.class, course.getCourseType()); + if (type == null) { + throw new BaseException("报名类型不存在或已变更"); + } + int currentFamilyNumber = Boolean.TRUE.equals(type.getIsBringFamily()) && Boolean.TRUE.equals(type.getIsAddFamily()) + ? familyUser.getMobileColumnsValue().size() : 0; + if (isSignFullForUpdate(course, type, currentFamilyNumber, currentSignUp, null)) { + throw new BaseException("当前报名人数已满"); + } + if (isSignFullForUpdate(course, type, currentFamilyNumber, currentSignUp, currentSignUp.getUnionId())) { + throw new BaseException("该活动您所在的分工会名额不足"); + } + if (!isSignCourseForUpdate(course, activity, currentSignUp.getId())) { + throw new BaseException(activity.getRestrictLimit() != null && activity.getRestrictLimit() == 3 + ? activity.getActivityName() + "限制报" + activity.getLimitNum() + "个活动,已达上限" + : "您选择的类型已达上限,不能再报该类型的了"); + } + validateCourseTime(course, familyUser.getActivityCourseId(), currentSignUp); + } + + @Override + public List getEditCourseTimeSelectList(String courseId, String familyUserId) { + FamilyUser currentSignUp = dao().fetch(FamilyUser.class, familyUserId); + if (currentSignUp == null || !Objects.equals(currentSignUp.getCourseId(), courseId) + || !Objects.equals(currentSignUp.getUserId(), SecurityUtil.getUserId())) { + throw new BaseException("未找到本人在该课程的报名信息"); + } + + List courseTimeList = dao().query(FamilyActivityCourse.class, + Cnd.where(FamilyActivityCourse::getCourseId, "=", courseId).asc(FamilyActivityCourse::getCourseStartTime)); + List normalUserList = dao().query(FamilyUser.class, Cnd.where(FamilyUser::getCourseId, "=", courseId) + .and(FamilyUser::getState, "in", List.of(1, 3)) + .and(FamilyUser::getId, "!=", currentSignUp.getId())); + List normalUserIds = normalUserList.stream().map(FamilyUser::getUserId).toList(); + + return courseTimeList.stream().map(courseTime -> { + int usedCount = Lang.isEmpty(normalUserIds) ? 0 : dao().count(FamilyUserCourse.class, + Cnd.where(FamilyUserCourse::getActivityCourseId, "=", courseTime.getId()) + .and(FamilyUserCourse::getCourseId, "=", courseId) + .and(FamilyUserCourse::getUserId, "in", normalUserIds)); + int limitCount = courseTime.getCourseLimitNum() != null ? courseTime.getCourseLimitNum() : 0; + int remainingNum = Math.max(limitCount - usedCount, 0); + NutMap option = NutMap.NEW(); + option.put("remainingNum", remainingNum); + option.put("text", DateUtil.format(courseTime.getCourseStartTime(), "HH:mm") + "至" + + DateUtil.format(courseTime.getCourseEndTime(), "HH:mm") + "段(剩" + remainingNum + ")"); + option.put("value", courseTime.getId()); + option.put("disabled", remainingNum == 0); + return option; + }).filter(option -> option.getInt("remainingNum") > 0).toList(); + } + + /** + * 锁定课程记录。所有正式报名都必须先取得该锁,确保名额统计与写入处于同一串行区间。 + */ + private void lockCourseForSignUp(String courseId) { + if (StrUtil.isBlank(courseId)) { + throw new BaseException("报名课程信息不能为空"); + } + Sql lockSql = Sqls.create("SELECT id FROM family_course WHERE id = @courseId FOR UPDATE"); + lockSql.setParam("courseId", courseId); + lockSql.setCallback(Sqls.callback.str()); + Object lockedCourseId = dao().execute(lockSql).getResult(); + if (lockedCourseId == null) { + throw new BaseException("报名课程不存在或已删除"); + } + } + + /** + * 预填新增、修改和删除共用时间及人员范围校验。 + * 预填只允许发生在报名开始时间之前,到达开始时间后数据立即冻结。 + */ + private void validatePrefillTimeAndScope(FamilyActivity activity, String userId) { + if (activity == null) { + throw new BaseException("报名活动不存在或已删除"); + } + if (activity.getActivitySignUpStartTime() == null) { + throw new BaseException("活动未配置报名开始时间,不能预填报名信息"); + } + if (DateUtil.compare(new Date(), activity.getActivitySignUpStartTime()) >= 0) { + throw new BaseException("报名已经开始,预填信息已冻结,不能继续修改"); + } + int scopeCount = dao().count(ActivityUserScope.class, + Cnd.where(ActivityUserScope::getGroupId, "=", activity.getActivityGroupId()) + .and(ActivityUserScope::getUserId, "=", userId)); + if (scopeCount == 0) { + throw new BaseException("抱歉,您没有此次活动的权限"); + } + } + + /** + * 预填时只校验时段归属和必选要求,不查询或占用时段名额。 + */ + private void validatePrefillCourseTime(FamilyCourse course, String activityCourseId) { + if (!Boolean.TRUE.equals(course.getCourseIsLimitApply())) { + return; + } + if (StrUtil.isBlank(activityCourseId)) { + throw new BaseException("请选择报名时段"); + } + FamilyActivityCourse courseTime = dao().fetch(FamilyActivityCourse.class, activityCourseId); + if (courseTime == null || !Objects.equals(courseTime.getCourseId(), course.getId())) { + throw new BaseException("报名时段不存在或已变更"); + } + } + + /** + * 根据课程类型当前动态字段配置重建子女数据,并在服务端完成必填、格式、年龄和性别校验。 + * 前端只负责提交 columnCode 与 columnValue,字段名称和控件类型均以服务端配置为准。 + */ + private List> normalizeAndValidateFamilyColumns(List> submittedFamilies, + FamilyType type, FamilyCourse course, FamilyActivity activity) { + if (StrUtil.isBlank(activity.getOnlyKey())) { + throw new BaseException("活动未配置家属唯一标识,请联系管理员"); + } + if (activity.getFamilyMaxCount() == null) { + throw new BaseException("活动未配置家属最多人数,请联系管理员"); + } + if (Lang.isEmpty(submittedFamilies)) { + throw new BaseException("请填写" + activity.getKeyWord() + "信息"); + } + if (activity.getFamilyMaxCount() != null && submittedFamilies.size() > activity.getFamilyMaxCount()) { + throw new BaseException(activity.getKeyWord() + "人数最多为" + activity.getFamilyMaxCount()); + } + dao().fetchLinks(type, "familyMobileSignColumnList", Cnd.NEW().asc("columnIndex")); + List columnConfigs = type.getFamilyMobileSignColumnList(); + if (Lang.isEmpty(columnConfigs)) { + throw new BaseException("报名类型未配置" + activity.getKeyWord() + "信息字段"); + } + + List> normalizedFamilies = new ArrayList<>(); + Set currentOnlyKeyValues = new HashSet<>(); + for (int familyIndex = 0; familyIndex < submittedFamilies.size(); familyIndex++) { + List submittedFamily = submittedFamilies.get(familyIndex); + Map submittedValueMap = Lang.isEmpty(submittedFamily) ? new HashMap<>() + : submittedFamily.stream().filter(Objects::nonNull) + .filter(item -> StrUtil.isNotBlank(item.getString("columnCode"))) + .collect(Collectors.toMap(item -> item.getString("columnCode"), item -> item, (left, right) -> left)); + List normalizedFamily = new ArrayList<>(); + for (FamilyMobileSignColumn config : columnConfigs) { + NutMap submittedColumn = submittedValueMap.get(config.getColumnCode()); + Object columnValue = submittedColumn == null ? null : submittedColumn.get("columnValue"); + validateDynamicColumnValue(config, columnValue, familyIndex, course, activity); + + NutMap normalizedColumn = NutMap.NEW(); + normalizedColumn.put("columnName", config.getColumnName()); + normalizedColumn.put("columnCode", config.getColumnCode()); + normalizedColumn.put("columnValue", columnValue); + normalizedColumn.put("columnFormType", config.getColumnFormType()); + normalizedFamily.add(normalizedColumn); + } + normalizedFamilies.add(normalizedFamily); + + NutMap onlyKeyColumn = normalizedFamily.stream() + .filter(item -> Objects.equals(activity.getOnlyKey(), item.getString("columnCode"))) + .findFirst().orElse(null); + if (onlyKeyColumn == null || StrUtil.isBlank(onlyKeyColumn.getString("columnValue"))) { + throw new BaseException(activity.getKeyWord() + (familyIndex + 1) + "缺少唯一标识信息"); + } + if (!currentOnlyKeyValues.add(onlyKeyColumn.getString("columnValue"))) { + throw new BaseException(activity.getKeyWord() + "不能重复选择同一人"); + } + } + return normalizedFamilies; + } + + /** + * 校验一个动态字段。参数为字段配置、用户值、子女序号、课程和活动;校验失败抛出业务异常,无返回值。 + */ + private void validateDynamicColumnValue(FamilyMobileSignColumn config, Object columnValue, int familyIndex, + FamilyCourse course, FamilyActivity activity) { + String displayName = activity.getKeyWord() + (familyIndex + 1) + "的" + config.getColumnName(); + boolean empty = columnValue == null || (columnValue instanceof String && StrUtil.isBlank((String) columnValue)) + || (columnValue instanceof Collection && ((Collection) columnValue).isEmpty()); + if (Boolean.TRUE.equals(config.getIsRequired()) && empty) { + throw new BaseException(displayName + "不能为空"); + } + if (empty || StrUtil.isBlank(config.getValidRule())) { + return; + } + String stringValue = String.valueOf(columnValue).trim(); + if ("mobile".equals(config.getValidRule()) && !Validator.isMobile(stringValue)) { + throw new BaseException(displayName + "格式不正确"); + } + if ("email".equals(config.getValidRule()) && !Validator.isEmail(stringValue)) { + throw new BaseException(displayName + "格式不正确"); + } + if (!"idCard".equals(config.getValidRule())) { + return; + } + if (!IdcardUtil.isValidCard(stringValue)) { + throw new BaseException(displayName + "格式不正确"); + } + if (course.isFamilyAgeLimit()) { + int age = IdcardUtil.getAgeByIdCard(stringValue); + if (course.getMinAge() != null && age < course.getMinAge()) { + throw new BaseException("限制报名最小年龄为" + course.getMinAge()); + } + if (course.getMaxAge() != null && age > course.getMaxAge()) { + throw new BaseException("限制报名最大年龄为" + course.getMaxAge()); + } + } + if (course.isFamilySexLimit() && StrUtil.isNotBlank(course.getFamilySex())) { + String sex = IdcardUtil.getGenderByIdCard(stringValue) == 1 ? "男" : "女"; + if (!Objects.equals(course.getFamilySex(), sex)) { + throw new BaseException("限制报名性别为" + course.getFamilySex()); + } + } + } + + /** + * 新增和修改共用活动时间及面向对象校验;update 仅用于区分业务提示文案。 + */ + private void validateSignUpTimeAndScope(FamilyActivity activity, String userId, boolean update) { + if (activity == null) { + throw new BaseException("报名活动不存在或已删除"); + } + Date now = new Date(); + if (activity.getActivitySignUpStartTime() == null || DateUtil.compare(now, activity.getActivitySignUpStartTime()) < 0) { + throw new BaseException(update ? "报名未开始,不能修改报名信息" : "报名未开始"); + } + if (activity.getActivitySignUpEndTime() == null || DateUtil.compare(now, activity.getActivitySignUpEndTime()) >= 0) { + throw new BaseException(update ? "报名已结束,不能修改报名信息" : "报名已结束"); + } + int scopeCount = dao().count(ActivityUserScope.class, Cnd.where(ActivityUserScope::getGroupId, "=", activity.getActivityGroupId()) + .and(ActivityUserScope::getUserId, "=", userId)); + if (scopeCount == 0) { + throw new BaseException("抱歉,您没有此次活动的权限"); + } + } + + /** + * 按新增报名的名额规则校验修改数据,并从正常或候补人数中扣除本人原报名贡献。 + */ + private boolean isSignFullForUpdate(FamilyCourse course, FamilyType type, int currentFamilyNumber, + FamilyUser currentSignUp, String unionId) { + if (course.getCoursePeopleNumber() == 0) { + return true; + } + + int limitCount = course.getCoursePeopleNumber(); + if (StrUtil.isNotBlank(unionId)) { + List unionLimitList = course.getUnionLimit(); + if (Lang.isEmpty(unionLimitList)) { + return false; + } + NutMap unionLimit = unionLimitList.stream() + .filter(item -> Objects.equals(item.getString("id"), unionId)).findFirst().orElse(null); + if (unionLimit == null) { + return true; + } + limitCount = unionLimit.getInt("limitCount"); + } + + int normalCount = StrUtil.isBlank(unionId) + ? statisticsService.queryCourseCount(course.getId(), course.getCourseType()) + : statisticsService.queryCourseCount(course.getId(), course.getCourseType(), unionId); + int waitCount = StrUtil.isBlank(unionId) + ? statisticsService.queryCourseWaitCount(course.getId(), course.getCourseType()) + : statisticsService.queryCourseWaitCount(course.getId(), course.getCourseType(), unionId); + int oldFamilyNumber = Lang.isNotEmpty(currentSignUp.getMobileColumnsValue()) + ? currentSignUp.getMobileColumnsValue().size() : 0; + int oldContribution = getFamilyContribution(type, oldFamilyNumber); + if (Objects.equals(currentSignUp.getState(), 1) || Objects.equals(currentSignUp.getState(), 3)) { + normalCount = Math.max(normalCount - oldContribution, 0); + } else if (Objects.equals(currentSignUp.getState(), 2)) { + waitCount = Math.max(waitCount - oldContribution, 0); + } + + int newContribution = getFamilyContribution(type, currentFamilyNumber); + if (limitCount - normalCount > 0) { + int reservedNumber = StrUtil.isBlank(unionId) ? course.getCourseReservedNumber() : 0; + return normalCount + newContribution + reservedNumber > limitCount; + } + int waitingNum = course.getWaitingNum() == null ? 0 : course.getWaitingNum(); + return waitCount + newContribution > waitingNum; + } + + /** + * 计算一条报名记录占用的总名额,规则与现有报名统计保持一致。 + */ + private int getFamilyContribution(FamilyType type, int familyNumber) { + int contribution = Boolean.TRUE.equals(type.getSelfAddFamily()) ? 1 : 0; + if (Boolean.TRUE.equals(type.getIsBringFamily()) && Boolean.TRUE.equals(type.getIsAddFamily())) { + contribution += familyNumber; + } + return contribution; + } + + /** + * 修改报名数量限制校验时排除当前报名记录,再把本次修改作为一条报名重新计算。 + */ + private boolean isSignCourseForUpdate(FamilyCourse course, FamilyActivity activity, String currentSignUpId) { + if (activity.getRestrictLimit() == null || activity.getRestrictLimit() == 1) { + return true; + } + if (activity.getRestrictLimit() == 2) { + FamilyTypeLimit typeLimit = dao().fetch(FamilyTypeLimit.class, Cnd.where(FamilyTypeLimit::getTypeId, "=", course.getCourseType()) + .and(FamilyTypeLimit::getActivityId, "=", activity.getId())); + if (typeLimit == null || typeLimit.getLimitNum() == 0) { + return true; + } + Sql sql = Sqls.create(""" + SELECT count(tsus.id) + FROM family_user tsus + LEFT JOIN family_course tsuc ON tsuc.id = tsus.courseId + WHERE tsuc.courseType = @courseType + AND tsus.userId = @userId + AND tsus.activityId = @activityId + AND tsus.id != @currentSignUpId + """); + sql.setParam("courseType", course.getCourseType()); + sql.setParam("userId", SecurityUtil.getUserId()); + sql.setParam("activityId", activity.getId()); + sql.setParam("currentSignUpId", currentSignUpId); + return count(sql) < typeLimit.getLimitNum(); + } + int activitySignCount = dao().count(FamilyUser.class, Cnd.where(FamilyUser::getActivityId, "=", activity.getId()) + .and(FamilyUser::getUserId, "=", SecurityUtil.getUserId()) + .and(FamilyUser::getId, "!=", currentSignUpId)); + return activity.getLimitNum() == null || activitySignCount < activity.getLimitNum(); + } + + /** + * 校验独立报名时段名额;修改时排除本人原记录,新增时直接按当前占用人数计算。 + */ + private void validateCourseTime(FamilyCourse course, String activityCourseId, FamilyUser currentSignUp) { + if (!Boolean.TRUE.equals(course.getCourseIsLimitApply())) { + return; + } + if (StrUtil.isBlank(activityCourseId)) { + throw new BaseException("请选择报名时段"); + } + FamilyActivityCourse activityCourse = dao().fetch(FamilyActivityCourse.class, activityCourseId); + if (activityCourse == null || !Objects.equals(activityCourse.getCourseId(), course.getId())) { + throw new BaseException("报名时段不存在或已变更"); + } + Cnd normalUserCnd = Cnd.where(FamilyUser::getCourseId, "=", course.getId()) + .and(FamilyUser::getState, "in", List.of(1, 3)); + if (currentSignUp != null) { + normalUserCnd.and(FamilyUser::getId, "!=", currentSignUp.getId()); + } + List normalUserList = dao().query(FamilyUser.class, normalUserCnd); + List normalUserIds = normalUserList.stream().map(FamilyUser::getUserId).toList(); + int usedCount = Lang.isEmpty(normalUserIds) ? 0 : dao().count(FamilyUserCourse.class, + Cnd.where(FamilyUserCourse::getActivityCourseId, "=", activityCourseId) + .and(FamilyUserCourse::getCourseId, "=", course.getId()) + .and(FamilyUserCourse::getUserId, "in", normalUserIds)); + if (activityCourse.getCourseLimitNum() == null || usedCount + 1 > activityCourse.getCourseLimitNum()) { + throw new BaseException("该时间段名额已报满,请选择其他时段报名"); + } + } + + /** + * 仅在报名时段发生变化时重建本人课程时段记录,未变化时保留原签到和领取信息。 + */ + private void syncUpdatedUserCourse(FamilyUser currentSignUp, String oldActivityCourseId) { + if (Objects.equals(oldActivityCourseId, currentSignUp.getActivityCourseId())) { + return; + } + dao().clear(FamilyUserCourse.class, Cnd.where(FamilyUserCourse::getActivityId, "=", currentSignUp.getActivityId()) + .and(FamilyUserCourse::getCourseId, "=", currentSignUp.getCourseId()) + .and(FamilyUserCourse::getUserId, "=", currentSignUp.getUserId())); + + List selectedCourseTimes; + if (StrUtil.isNotBlank(currentSignUp.getActivityCourseId())) { + FamilyActivityCourse selected = dao().fetch(FamilyActivityCourse.class, currentSignUp.getActivityCourseId()); + selectedCourseTimes = selected == null ? new ArrayList<>() : List.of(selected); + } else { + selectedCourseTimes = dao().query(FamilyActivityCourse.class, + Cnd.where(FamilyActivityCourse::getCourseId, "=", currentSignUp.getCourseId())); + } + List userCourseList = selectedCourseTimes.stream().map(courseTime -> { + FamilyUserCourse userCourse = new FamilyUserCourse(); + userCourse.setActivityId(currentSignUp.getActivityId()); + userCourse.setCourseId(currentSignUp.getCourseId()); + userCourse.setUserId(currentSignUp.getUserId()); + userCourse.setCourseStartTime(courseTime.getCourseStartTime()); + userCourse.setCourseEndTime(courseTime.getCourseEndTime()); + userCourse.setAttend(false); + userCourse.setActivityCourseId(courseTime.getId()); + return userCourse; + }).toList(); + if (Lang.isNotEmpty(userCourseList)) { + dao().insert(userCourseList); + } + } + @Async @Override public void asyncInsertUserCourse(String activityId, String courseId, String userId) { log.info("异步插入{}的上课信息,课程ID为{},活动ID为{}", userId, courseId, activityId); + insertUserCourse(activityId, courseId, userId); + } + + /** + * 同步写入报名人员的全部课程时段,用于保证正式报名主记录与时段记录处于同一事务。 + */ + private void insertUserCourse(String activityId, String courseId, String userId) { List courseList = dao().query(FamilyActivityCourse.class, Cnd.where("courseId", "=", courseId)); List list = new ArrayList<>(); courseList.forEach(v -> { @@ -313,7 +1059,9 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i userCourse.setActivityCourseId(v.getId()); list.add(userCourse); }); - dao().insert(list); + if (Lang.isNotEmpty(list)) { + dao().insert(list); + } } /** @@ -379,9 +1127,26 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i public Map validFamilyCount(FamilyUser user) { String activityId = user.getActivityId(); // 查询报名记录 - List list = dao().query(FamilyUser.class, Cnd.where(FamilyUser::getActivityId, "=", activityId).and(FamilyUser::getUserId, "=", SecurityUtil.getUserId())); + Cnd signUpCnd = Cnd.where(FamilyUser::getActivityId, "=", activityId) + .and(FamilyUser::getUserId, "=", SecurityUtil.getUserId()); + if (StrUtil.isNotBlank(user.getId())) { + signUpCnd.and(FamilyUser::getId, "!=", user.getId()); + } + List list = dao().query(FamilyUser.class, signUpCnd); FamilyActivity activity = dao().fetch(FamilyActivity.class, activityId); + if (activity == null) { + return Map.of(false, "报名活动不存在或已删除"); + } + if (StrUtil.isBlank(activity.getOnlyKey())) { + return Map.of(false, "活动未配置家属唯一标识,请联系管理员"); + } + if (activity.getFamilyMaxCount() == null) { + return Map.of(false, "活动未配置家属最多人数,请联系管理员"); + } + if (Lang.isEmpty(user.getMobileColumnsValue())) { + return Map.of(false, "请填写" + activity.getKeyWord() + "信息"); + } List> allMobileColumnsValue = new ArrayList<>(list.stream() .map(FamilyUser::getMobileColumnsValue) @@ -395,8 +1160,10 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i //return allMobileColumnsValue.size() > activity.getFamilyMaxCount(); long count = allMobileColumnsValue.stream() + .filter(Objects::nonNull) .flatMap(List::stream) - .filter(nutMap -> activity.getOnlyKey().equals(nutMap.getString("columnCode"))) + .filter(Objects::nonNull) + .filter(nutMap -> Objects.equals(activity.getOnlyKey(), nutMap.getString("columnCode"))) .map(nutMap -> nutMap.getString("columnValue")) .filter(Objects::nonNull) .distinct() @@ -420,7 +1187,11 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i String tempValue = ""; // 循环家属 for (List listMap : currentValue) { - NutMap nutMap = listMap.stream().filter(o -> o.getString("columnCode").equals(activity.getOnlyKey())).findFirst().orElse(null); + if (Lang.isEmpty(listMap)) { + continue; + } + NutMap nutMap = listMap.stream().filter(Objects::nonNull) + .filter(o -> Objects.equals(o.getString("columnCode"), activity.getOnlyKey())).findFirst().orElse(null); if(nutMap == null) { continue; } @@ -435,8 +1206,15 @@ public class FamilyActivityServiceImpl extends BaseServiceImpl i for (FamilyUser hisRecord : list) { List> hisValueList = hisRecord.getMobileColumnsValue(); + if (Lang.isEmpty(hisValueList)) { + continue; + } for (List hisValue : hisValueList) { - NutMap hisNutMap = hisValue.stream().filter(o -> o.getString("columnCode").equals(activity.getOnlyKey())).findFirst().orElse(null); + if (Lang.isEmpty(hisValue)) { + continue; + } + NutMap hisNutMap = hisValue.stream().filter(Objects::nonNull) + .filter(o -> Objects.equals(o.getString("columnCode"), activity.getOnlyKey())).findFirst().orElse(null); if(hisNutMap == null) { continue; } diff --git a/src/main/resources/static/assets/platform/js/util/h5PopupHistory.js b/src/main/resources/static/assets/platform/js/util/h5PopupHistory.js new file mode 100644 index 0000000..7acfab7 --- /dev/null +++ b/src/main/resources/static/assets/platform/js/util/h5PopupHistory.js @@ -0,0 +1,76 @@ +const h5PopupHistory = (() => { + const popupStack = [] + let skipNextPop = false + + const handlePopState = (event) => { + if (skipNextPop) { + skipNextPop = false + event.stopImmediatePropagation() + return + } + if (popupStack.length === 0) return + event.stopImmediatePropagation() + const popup = popupStack.pop() + popup.closeFromHistory() + } + + window.addEventListener("popstate", handlePopState) + + return { + /** + * 注册已打开的 H5 弹框并增加一层历史记录。 + * @param id 弹框唯一标识,同一标识不会重复入栈 + * @param owner 页面或组件标识,用于提交成功及销毁时统一清理 + * @param closeFromHistory 返回键触发时关闭弹框的回调 + * @return 无返回值 + */ + open(id, owner, closeFromHistory) { + if (popupStack.some((item) => item.id === id)) return + popupStack.push({id: id, owner: owner, closeFromHistory: closeFromHistory}) + const state = Object.assign({}, window.history.state || {}, { + h5PopupId: id, + url: window.location.href + }) + window.history.pushState(state, "", window.location.href) + }, + + /** + * 通过取消、关闭按钮关闭指定弹框时,同步回退对应历史记录。 + * @param id 弹框唯一标识,必须是当前最上层弹框 + * @return boolean;true 表示已发起历史回退,false 表示弹框不在栈顶 + */ + close(id) { + const top = popupStack[popupStack.length - 1] + if (!top || top.id !== id) return false + window.history.back() + return true + }, + + /** + * 提交成功前清理当前组件的全部弹框及其历史记录。 + * @param owner 页面或组件标识 + * @return 无返回值 + */ + clearOwner(owner) { + const ownerPopups = popupStack.filter((item) => item.owner === owner) + if (ownerPopups.length === 0) return + ownerPopups.slice().reverse().forEach((item) => item.closeFromHistory()) + for (let i = popupStack.length - 1; i >= 0; i--) { + if (popupStack[i].owner === owner) popupStack.splice(i, 1) + } + skipNextPop = true + window.history.go(-ownerPopups.length) + }, + + /** + * 组件销毁时注销回调,防止旧组件被历史返回事件再次调用。 + * @param owner 页面或组件标识 + * @return 无返回值 + */ + unregisterOwner(owner) { + for (let i = popupStack.length - 1; i >= 0; i--) { + if (popupStack[i].owner === owner) popupStack.splice(i, 1) + } + } + } +})() diff --git a/src/main/resources/views/layouts/platform_h5.html b/src/main/resources/views/layouts/platform_h5.html index 4de2c44..46a6243 100644 --- a/src/main/resources/views/layouts/platform_h5.html +++ b/src/main/resources/views/layouts/platform_h5.html @@ -57,6 +57,7 @@ + diff --git a/src/main/resources/views/platform/zhgh/activity/family/apply/courseList.js b/src/main/resources/views/platform/zhgh/activity/family/apply/courseList.js index 6647a2a..822ad58 100644 --- a/src/main/resources/views/platform/zhgh/activity/family/apply/courseList.js +++ b/src/main/resources/views/platform/zhgh/activity/family/apply/courseList.js @@ -74,10 +74,21 @@ const courseList = { - + @@ -150,10 +161,63 @@ const courseList = { pdf: false, infoVisible: false, introduce: '', + nowTime: Date.now(), + signTimeTimer: null, } }, + computed: { + // 只有严格早于报名开始时间时才允许维护预填数据。 + isBeforeSignUpTime() { + return this.activity.activitySignUpStartTime + && this.$moment(this.nowTime).isBefore(this.$moment(this.activity.activitySignUpStartTime)) + }, + // 修改报名与我要报名共用同一时间范围:开始时间已到且尚未达到结束时间。 + isInSignUpTime() { + const now = this.$moment(this.nowTime) + return this.activity.activitySignUpStartTime + && this.activity.activitySignUpEndTime + && !now.isBefore(this.$moment(this.activity.activitySignUpStartTime)) + && now.isBefore(this.$moment(this.activity.activitySignUpEndTime)) + }, + }, methods: { - async onPreview(introduce) { + onPrefill(row) { + const courseType = this.courseTypeList.find((item) => item.id === row.courseType) + this.tableLoading = true + this.$axios.post("/platform/family/apply/getPrefill", {courseId: row.id}) + .then((resp) => { + if (resp.code !== 0) { + this.$message.warning(resp.msg) + return + } + this.$refs.signFormRef.onOpen(row, courseType, this.activity, resp.data, "prefill") + }) + .finally(() => { + this.tableLoading = false + }) + }, + onQuickSignUp(row) { + this.$confirm("将使用报名开始前保存的预填信息直接报名,是否继续?", "提示", { + confirmButtonText: "确定", + cancelButtonText: "取消", + type: "warning" + }).then(() => { + this.tableLoading = true + this.$axios.post("/platform/family/apply/quickSignUp", {courseId: row.id}) + .then((resp) => { + if (resp.code === 0) { + this.$alert(resp.msg, "提示", {confirmButtonText: "确定", type: "success"}) + this.pageData() + } else { + this.$message.warning(resp.msg) + } + }) + .finally(() => { + this.tableLoading = false + }) + }).catch(() => {}) + }, + onPreview(introduce) { try { const parser = new DOMParser(); const doc = parser.parseFromString(introduce, 'text/html'); @@ -161,10 +225,16 @@ const courseList = { const href = link.getAttribute('href'); const id = href.substring(href.indexOf("=") + 1) - const res = await this.$axios.post("/platform/sys/file/previewFileData", { ids: JSON.stringify([id]) }) - - this.pdf = true - window.open("/assets/platform/plugins/pdfJs/web/viewer.html?file=" + encodeURIComponent(res.data[0].downloadPath), res.data[0].name) + this.$axios.post("/platform/sys/file/previewFileData", {ids: JSON.stringify([id])}) + .then((res) => { + this.pdf = true + window.open("/assets/platform/plugins/pdfJs/web/viewer.html?file=" + encodeURIComponent(res.data[0].downloadPath), res.data[0].name) + }) + .catch(() => { + this.introduce = introduce + this.pdf = false + this.infoVisible = true + }) } catch (e) { this.introduce = introduce this.pdf = false @@ -180,12 +250,14 @@ const courseList = { } this.doSearch() }, - async onOpen(row) { + onOpen(row) { this.activity = row this.$set(this.pageForm, 'activityId', row.id) - await this.pageData() - await this.getCourseTypeList() - this.queryCourseAssort() + this.pageData() + .then(() => this.getCourseTypeList()) + .then(() => { + this.queryCourseAssort() + }) }, onSign(row) { const courseType = this.courseTypeList.find((v) => v.id === row.courseType) @@ -208,34 +280,64 @@ const courseList = { } }) }, + onEdit(row) { + const courseType = this.courseTypeList.find((item) => item.id === row.courseType) + this.tableLoading = true + this.$axios.post("/platform/family/apply/validateUpdateSignUp", {courseId: row.id}) + .then((validateRes) => { + if (validateRes.code !== 0) { + this.$message.warning(validateRes.msg) + return null + } + return this.$axios.post("/platform/family/apply/getMySignUp", {courseId: row.id}) + }) + .then((signUpRes) => { + if (!signUpRes) return + if (signUpRes.code !== 0) { + this.$message.warning(signUpRes.msg) + return + } + this.$refs.signFormRef.onOpen(row, courseType, this.activity, signUpRes.data) + }) + .finally(() => { + this.tableLoading = false + }) + }, onCancel(row) { this.$confirm("您确定要取消吗?", "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "info" - }).then(async () => { - const resp = await this.$axios.post("/platform/family/apply/cancelSignUp", { + }).then(() => { + this.$axios.post("/platform/family/apply/cancelSignUp", { activityId: row.activityId, courseId: row.id }) - if (resp.code === 0) { - await this.pageData() - this.$message.success(resp.msg) - } else { - this.$message.warning(resp.msg) - } + .then((resp) => { + if (resp.code !== 0) { + this.$message.warning(resp.msg) + return + } + this.$message.success(resp.msg) + return this.pageData() + }) }) }, - async getCourseTypeList() { - const resp = await this.$axios.post("/platform/family/type/getAllType") - if (resp.code === 0) { - this.courseTypeList = resp.data - } + getCourseTypeList() { + return this.$axios.post("/platform/family/type/getAllType") + .then((resp) => { + if (resp.code === 0) { + this.courseTypeList = resp.data + } + return resp + }) }, - async openViewCourseTime(id) { - const resp = await this.$axios.post(loc() + "/getCourseTime", { id: id }) - this.courseTimeList = resp.data - this.courseTimeListDialog = true + openViewCourseTime(id) { + this.$axios.post(loc() + "/getCourseTime", {id: id}) + .then((resp) => { + this.courseTimeList = resp.data + this.courseTimeListDialog = true + }) }, openViewMap(point) { if (!Array.isArray(point)) { @@ -270,16 +372,19 @@ const courseList = { return "余" + lave +"/" + o.coursePeopleNumber + "人" } }, - async pageData() { + pageData() { const pageForm = clone({...this.pageForm}) pageForm.assortTypes = JSON.stringify(pageForm.assortTypes) - const resp = await this.$axios.post(loc() + "/pageData", pageForm) - if (resp.code === 0) { - this.tableData = resp.data.list - this.pageForm.totalCount = resp.data.totalCount - } else { - this.$message.warning(resp.msg) - } + return this.$axios.post(loc() + "/pageData", pageForm) + .then((resp) => { + if (resp.code === 0) { + this.tableData = resp.data.list + this.pageForm.totalCount = resp.data.totalCount + } else { + this.$message.warning(resp.msg) + } + return resp + }) }, queryCourseAssort() { this.$axios.post(loc() + "/queryCourseAssort", {activityId: this.activity.id}) @@ -289,7 +394,13 @@ const courseList = { }, }, created() { - + // 页面停留到报名开始时间时自动切换为正式报名按钮,无需用户手工刷新。 + this.signTimeTimer = window.setInterval(() => { + this.nowTime = Date.now() + }, 1000) + }, + beforeDestroy() { + if (this.signTimeTimer) window.clearInterval(this.signTimeTimer) }, style: /*language=CSS*/ ` .glow-box { diff --git a/src/main/resources/views/platform/zhgh/activity/family/apply/index.html b/src/main/resources/views/platform/zhgh/activity/family/apply/index.html index 630c849..5e2f735 100644 --- a/src/main/resources/views/platform/zhgh/activity/family/apply/index.html +++ b/src/main/resources/views/platform/zhgh/activity/family/apply/index.html @@ -170,10 +170,7 @@ layout("/layouts/platform.html"){ }) }, onOpen(row) { - if(this.$store.state.user.loginname !== '45066' && this.$moment().isBefore(this.$moment(row.activitySignUpStartTime))) { - this.onView(row) - return - } + // 报名前允许进入课程列表维护预填信息,正式报名仍由后端报名时间校验控制。 this.infoVisible = false this.$refs.guava.view(() => { this.$refs.courseListRef.onOpen(row) @@ -188,7 +185,7 @@ layout("/layouts/platform.html"){ }) }, }, - async created() { + created() { this.pageData() } }) 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 23eb610..13047bd 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 @@ -1,7 +1,8 @@ const signForm = { template: /*language=HTML*/ `
-
个人信息
@@ -36,7 +37,8 @@ const signForm = { - + @@ -137,7 +139,7 @@ const signForm = { 取 消 - 提 交 + {{prefillMode ? '保存预填' : '提 交'}}
@@ -147,6 +149,9 @@ const signForm = { data() { return { signDialog: false, + editMode: false, + prefillMode: false, + formLoading: false, formData: { mobileColumnsValue: [], }, @@ -172,58 +177,79 @@ const signForm = { const ac = (Number(this.active) - 1) this.active = '' + (ac >= 0 ? ac : 0) }, - async validSignUp() { + validSignUp() { //获取家属是多少人 - const res = await this.$axios.post("/platform/family/apply/validateSignUp", { + return this.$axios.post("/platform/family/apply/validateSignUp", { courseId: this.formData.courseId, currentFamilyNumber: this.formData.mobileColumnsValue.length || 0 }) - if (res.code !== 0) { - this.$alert(res.msg, "提示", { - confirmButtonText: "确定", - type: "warning" + .then((res) => { + if (res.code !== 0) { + this.$alert(res.msg, "提示", { + confirmButtonText: "确定", + type: "warning" + }) + return false + } + return true }) - return false - } - return true }, - async validateSourceSignUp() { - if (this.courseRow.courseIsLimitApply) { - const resp = await $.post('/platform/family/apply/validateSourceSignUp', { + validateSourceSignUp() { + if (!this.courseRow.courseIsLimitApply) { + return Promise.resolve(true) + } + return $.post('/platform/family/apply/validateSourceSignUp', { activityCourseId: this.formData.activityCourseId, courseId: this.courseRow.id }) - if (resp.code !== 0) { - this.$alert(resp.msg, "提示", { - confirmButtonText: "确定", - type: "warning" - }) - return false - } - } - return true - }, - async getCourseTimeSelectList(o) { - const resp = await $.post('/platform/family/apply/getCourseTimeSelectList', {courseId: o.id}) - if (resp.code === 0) { - this.courseTimeSelectList = resp.data - } else { - this.$message.warning('获取时段信息失败,请联系管理员') - } - }, - async onOpen(row, courseType, activity) { - this.initData(row, courseType, activity) - if (row.courseIsLimitApply) { - await this.getCourseTimeSelectList(row) - } - if (courseType && courseType.familyMobileSignColumnList.length > 0) { - courseType.familyMobileSignColumnList.forEach(item => { - item.columnValue = '' + .then((resp) => { + if (resp.code !== 0) { + this.$alert(resp.msg, "提示", { + confirmButtonText: "确定", + type: "warning" + }) + return false + } + return true }) + .always(() => {}) + }, + getCourseTimeSelectList(o, familyUserId) { + const url = familyUserId + ? "/platform/family/apply/getEditCourseTimeSelectList" + : "/platform/family/apply/getCourseTimeSelectList" + this.formLoading = true + return $.post(url, {courseId: o.id, familyUserId: familyUserId}) + .then((resp) => { + if (resp.code === 0) { + this.$set(this, "courseTimeSelectList", resp.data) + } else { + this.$message.warning(resp.msg || "获取时段信息失败,请联系管理员") + } + return resp + }) + .always(() => { + this.formLoading = false + }) + }, + onOpen(row, courseType, activity, signUpData, mode) { + this.prefillMode = mode === "prefill" + this.initData(row, courseType, activity, signUpData) + this.$set(this, "courseRow", row) + this.$set(this, "courseTypeRow", courseType) + const openDialog = () => { + this.$set(this, "signDialog", true) } - this.courseRow = row - this.courseTypeRow = courseType - this.signDialog = true + if (row.courseIsLimitApply) { + this.getCourseTimeSelectList(row, this.editMode && signUpData ? signUpData.id : "") + .then((resp) => { + if (resp.code === 0) { + openDialog() + } + }) + return + } + openDialog() }, validateIdCard(idCard) { if (!idCard) { @@ -354,60 +380,100 @@ const signForm = { return { age, sex, }; }, - async onSubmit() { + onSubmit() { //验证家属表单 if (!this.validFamilyForm()) return - if (!await this.validSignUp()) return - if (!await this.validateSourceSignUp()) return - this.$refs["form"].validate((valid) => { - if (valid) { - this.$confirm("您确定要提交吗?", "提示", { - confirmButtonText: "确定", - cancelButtonText: "取消", - type: "warning" - }).then(async () => { - const formData = clone(this.formData) - let array = [] - formData.mobileColumnsValue.forEach(item => { - const o = item.map((v) => { - return { - columnName: v.columnName, - columnValue: v.columnValue, - columnCode: v.columnCode, - columnFormType: v.columnFormType - } + const signUpValidation = (this.editMode || this.prefillMode) ? Promise.resolve(true) : this.validSignUp() + signUpValidation.then((signUpValid) => { + if (!signUpValid) return + const sourceValidation = (this.editMode || this.prefillMode) ? Promise.resolve(true) : this.validateSourceSignUp() + sourceValidation.then((sourceValid) => { + if (!sourceValid) return + this.$refs["form"].validate((valid) => { + if (!valid) return + this.$confirm("您确定要提交吗?", "提示", { + confirmButtonText: "确定", + cancelButtonText: "取消", + type: "warning" + }).then(() => { + const formData = clone(this.formData) + const array = formData.mobileColumnsValue.map((item) => { + return item.map((column) => { + return { + columnName: column.columnName, + columnValue: column.columnValue, + columnCode: column.columnCode, + columnFormType: column.columnFormType + } + }) }) - array.push(o) + formData.mobileColumnsValue = JSON.stringify(clone(array)) + const url = this.prefillMode + ? "/platform/family/apply/savePrefill" + : (this.editMode + ? "/platform/family/apply/updateSignUp" + : "/platform/family/apply/doSignUp") + this.formLoading = true + this.$axios.post(url, formData) + .then((resp) => { + if (resp.code === 0) { + this.$alert(resp.msg, "提示", { + confirmButtonText: "确定", + type: "warning" + }) + this.signDialog = false + this.$emit("refresh") + } else { + this.$message.warning(resp.msg) + } + }) + .finally(() => { + this.formLoading = false + }) }) - formData.mobileColumnsValue = JSON.stringify(clone(array)) - const resp = await this.$axios.post("/platform/family/apply/doSignUp", formData) - if (resp.code === 0) { - this.$alert(resp.msg, "提示", { - confirmButtonText: "确定", - type: "warning" - }) - this.signDialog = false - this.$emit('refresh') - } else { - this.$message.warning(resp.msg) - } }) - } + }) }) }, - initData(row, courseType, activity) { - this.activity = activity - this.formData = { + // 修改时按 columnCode 将历史值合并进当前字段配置,保留最新必填和格式校验规则。 + mergeMobileColumnsValue(courseType, mobileColumnsValue) { + const columnConfig = courseType && Array.isArray(courseType.familyMobileSignColumnList) + ? courseType.familyMobileSignColumnList : [] + return (mobileColumnsValue || []).map((family) => { + if (columnConfig.length === 0) { + return clone(family) + } + return clone(columnConfig).map((column) => { + const oldColumn = family.find((item) => item.columnCode === column.columnCode) + this.$set(column, "columnValue", oldColumn ? oldColumn.columnValue : "") + return column + }) + }) + }, + initData(row, courseType, activity, signUpData) { + this.$set(this, "activity", activity) + this.editMode = !this.prefillMode && Boolean(signUpData && signUpData.id) + // 部分历史账号未关联单位或分工会,初始化报名表单时需兼容关联对象为空。 + const currentUser = this.$store.state.user || {} + const formData = { activityId: row.activityId, courseId: row.id, - username: this.$store.state.user.username, - loginname: this.$store.state.user.loginname, - unionName: this.$store.state.user.union.name, - unitName: this.$store.state.user.unit.name, - sex: this.$store.state.user.sex, - mobile: this.$store.state.user.mobile, + username: currentUser.username, + loginname: currentUser.loginname, + unionName: currentUser.union ? currentUser.union.name : "", + unitName: currentUser.unit ? currentUser.unit.name : "", + sex: currentUser.sex, + mobile: currentUser.mobile, mobileColumnsValue: [], } + if ((this.editMode || this.prefillMode) && signUpData) { + this.$set(formData, "id", signUpData.id) + this.$set(formData, "mobile", signUpData.mobile) + this.$set(formData, "activityCourseId", signUpData.activityCourseId) + this.$set(formData, "mobileColumnsValue", + this.mergeMobileColumnsValue(courseType, signUpData.mobileColumnsValue)) + } + this.$set(this, "formData", formData) this.$nextTick(() => { this.$refs.form.clearValidate() }) diff --git a/src/main/resources/views/platform/zhghh5/activity/family/apply/index.html b/src/main/resources/views/platform/zhghh5/activity/family/apply/index.html index 6f4d76e..05ad771 100644 --- a/src/main/resources/views/platform/zhghh5/activity/family/apply/index.html +++ b/src/main/resources/views/platform/zhghh5/activity/family/apply/index.html @@ -145,10 +145,7 @@ layout("/layouts/platform_h5.html"){ this.infoVisible = true }, onApply(row) { - if(this.$store.state.user.loginname !== '45066' && this.$moment().isBefore(this.$moment(row.activitySignUpStartTime))) { - this.onView(row) - return - } + // 报名前允许进入课程列表维护预填信息,预填不会提前占用名额。 this.$pjaxReplace('/platform/family/apply/list/h5?id=' + row.id) }, onReady() { 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 81e5df7..0a56153 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 @@ -3,7 +3,9 @@ const applyForm = { /*language=HTML*/ `
- + @@ -16,22 +18,25 @@ const applyForm = { + @@ -65,7 +70,7 @@ const applyForm = {
- 提交 + {{prefillMode ? '保存预填' : '提交'}}
@@ -77,15 +82,25 @@ const applyForm = { row: {}, activity: {}, visible: false, + editMode: false, + prefillMode: false, + formLoading: false, formData: {}, showCoursePicker: false, courseTimeSelectList: [], courseType: {}, + historyOwner: '', } }, components: { "train-dynamic-form": httpVueLoader("/components/module/trainSignUp/TrainDynamicForm.vue") }, + mounted() { + this.historyOwner = "family-apply-form-" + this._uid + }, + beforeDestroy() { + h5PopupHistory.unregisterOwner(this.historyOwner) + }, methods: { addFamily() { if(this.formData.mobileColumnsValue.length >= this.activity.familyMaxCount) { @@ -100,40 +115,110 @@ const applyForm = { const ac = (Number(this.familyActive) - 1) this.familyActive = '' + (ac >= 0 ? ac : 0) }, - async onOpen(row, courseType, activity) { - this.row = row - this.courseType = courseType - this.activity = activity - - this.init(row, courseType) - if(row.courseIsLimitApply) { - await this.getCourseTimeSelectList(row) + onOpen(row, courseType, activity, signUpData, mode) { + this.prefillMode = mode === "prefill" + this.$set(this, "row", row) + this.$set(this, "courseType", courseType) + this.$set(this, "activity", activity) + this.init(row, courseType, signUpData) + const openForm = () => { + h5PopupHistory.open(this.historyOwner + "-form", this.historyOwner, () => { + this.$set(this, "visible", false) + }) + this.$set(this, "visible", true) } - this.visible = true + if(row.courseIsLimitApply) { + this.getCourseTimeSelectList(row, this.editMode && signUpData ? signUpData.id : "") + .then((resp) => { + if (resp.code !== 0) return + this.setCourseTimeName(signUpData ? signUpData.activityCourseId : "") + openForm() + }) + return + } + openForm() }, - init(row, courseType) { - this.$set(this.formData, 'username', this.$store.state.user.username) - this.$set(this.formData, 'loginname', this.$store.state.user.loginname) - this.$set(this.formData, 'unionName', this.$store.state.user.union.name) - this.$set(this.formData, 'unitName', this.$store.state.user.unit.name) - this.$set(this.formData, 'sex', this.$store.state.user.sex) - this.$set(this.formData, 'activityId', row.activityId) - this.$set(this.formData, 'courseId', row.id) - this.$set(this.formData, 'mobileColumnsValue', []) + init(row, courseType, signUpData) { + this.editMode = !this.prefillMode && Boolean(signUpData && signUpData.id) + this.$set(this, "familyActive", "") + this.$set(this, "courseTimeSelectList", []) + this.$set(this, "showCoursePicker", false) + const formData = { + username: this.$store.state.user.username, + loginname: this.$store.state.user.loginname, + unionName: this.$store.state.user.union.name, + unitName: this.$store.state.user.unit.name, + sex: this.$store.state.user.sex, + mobile: this.$store.state.user.mobile, + activityId: row.activityId, + courseId: row.id, + mobileColumnsValue: [] + } + if ((this.editMode || this.prefillMode) && signUpData) { + this.$set(formData, "id", signUpData.id) + this.$set(formData, "mobile", signUpData.mobile) + this.$set(formData, "activityCourseId", signUpData.activityCourseId) + this.$set(formData, "mobileColumnsValue", + this.mergeMobileColumnsValue(courseType, signUpData.mobileColumnsValue)) + } + this.$set(this, "formData", formData) }, onCourseConfirm(val){ - this.formData.activityCourseId = val.value this.$set(this.formData, "activityCourseId", val.value) this.$set(this.formData, "courseTimeName", val.text.substring(0, 12)) - this.showCoursePicker = false + this.closeCoursePicker() }, - async getCourseTimeSelectList(o) { - const resp = await this.$axios.post('/platform/family/apply/getCourseTimeSelectList',{courseId: o.id}) - if (resp.code === 0) { - this.courseTimeSelectList = resp.data - } else { - this.$toast.fail("获取时段信息失败,请联系管理员") - } + getCourseTimeSelectList(o, familyUserId) { + const url = familyUserId + ? "/platform/family/apply/getEditCourseTimeSelectList" + : "/platform/family/apply/getCourseTimeSelectList" + this.formLoading = true + return this.$axios.post(url, {courseId: o.id, familyUserId: familyUserId}) + .then((resp) => { + if (resp.code === 0) { + this.$set(this, "courseTimeSelectList", resp.data) + } else { + this.$toast.fail(resp.msg || "获取时段信息失败,请联系管理员") + } + return resp + }) + .finally(() => { + this.formLoading = false + }) + }, + // 打开时段选择器时增加一层历史,返回键只关闭当前选择器。 + openCoursePicker() { + h5PopupHistory.open(this.historyOwner + "-course-picker", this.historyOwner, () => { + this.$set(this, "showCoursePicker", false) + }) + this.$set(this, "showCoursePicker", true) + }, + closeCoursePicker() { + h5PopupHistory.close(this.historyOwner + "-course-picker") + }, + onCoursePickerClosed() { + h5PopupHistory.close(this.historyOwner + "-course-picker") + }, + onFormClosed() { + h5PopupHistory.close(this.historyOwner + "-form") + }, + setCourseTimeName(activityCourseId) { + if (!activityCourseId) return + const selected = this.courseTimeSelectList.find((item) => item.value === activityCourseId) + if (selected) this.$set(this.formData, "courseTimeName", selected.text.substring(0, 12)) + }, + // 修改时按字段编码合并历史值,继续使用活动当前配置的必填和格式规则。 + mergeMobileColumnsValue(courseType, mobileColumnsValue) { + const columnConfig = courseType && Array.isArray(courseType.familyMobileSignColumnList) + ? courseType.familyMobileSignColumnList : [] + return (mobileColumnsValue || []).map((family) => { + if (columnConfig.length === 0) return clone(family) + return clone(columnConfig).map((column) => { + const oldColumn = family.find((item) => item.columnCode === column.columnCode) + this.$set(column, "columnValue", oldColumn ? oldColumn.columnValue : "") + return column + }) + }) }, validateIdCard(idCard) { if (!idCard) { @@ -266,62 +351,88 @@ const applyForm = { return { age, sex, }; }, - async validateSignUp() { + validateSignUp() { // 获取家属人数 - const res = await this.$axios.post("/platform/family/apply/validateSignUp", { + return this.$axios.post("/platform/family/apply/validateSignUp", { courseId: this.formData.courseId, currentFamilyNumber: this.formData.mobileColumnsValue.length || 0 }) - if(res.code !== 0) { - this.$dialog.alert({title: '温馨提示', message: res.msg}) - } - return res.code === 0 + .then((res) => { + if(res.code !== 0) { + this.$dialog.alert({title: '温馨提示', message: res.msg}) + } + return res.code === 0 + }) }, - async validateCourseTime() { - const res = await this.$axios.post('/platform/family/apply/validateSourceSignUp', { + validateUpdateSignUp() { + return this.$axios.post("/platform/family/apply/validateUpdateSignUp", { + courseId: this.formData.courseId + }).then((res) => { + if(res.code !== 0) { + this.$toast.fail(res.msg) + } + return res.code === 0 + }) + }, + validateCourseTime() { + return this.$axios.post('/platform/family/apply/validateSourceSignUp', { activityCourseId: this.formData.activityCourseId, courseId: this.row.id }) - if(res.code !== 0) { - this.$dialog.alert({title: '温馨提示', message: res.msg}) - } - return res.code === 0 - }, - async onSubmit() { - if (!this.validFamilyForm()) return - if (!await this.validateSignUp()) return - - this.$refs.formRef.validate().then(() => { - this.$dialog.confirm({ - title: "提示", - message: "您确定要提交吗?" - }).then(async () => { - if (this.row.courseIsLimitApply) { - if(!await this.validateCourseTime()) return - } - - const formData = clone(this.formData) - let array = [] - formData.mobileColumnsValue.forEach(item => { - const o = item.map((v) => { - return { - columnName: v.columnName, - columnValue: v.columnValue, - columnCode: v.columnCode, - columnFormType: v.columnFormType - } - }) - array.push(o) - }) - formData.mobileColumnsValue = JSON.stringify(clone(array)) - this.$axios.post("/platform/family/apply/doSignUp", formData).then(res => { + .then((res) => { + if(res.code !== 0) { this.$dialog.alert({title: '温馨提示', message: res.msg}) - .then(() => { - if (res.code === 0) { - this.visible = false - this.$emit('refresh') - } + } + return res.code === 0 + }) + }, + onSubmit() { + if (!this.validFamilyForm()) return + const eligibility = this.prefillMode + ? Promise.resolve(true) + : (this.editMode ? this.validateUpdateSignUp() : this.validateSignUp()) + eligibility.then((eligible) => { + if (!eligible) return + this.$refs.formRef.validate().then(() => { + this.$dialog.confirm({ + title: "提示", + message: "您确定要提交吗?" + }).then(() => { + const courseTimeValidation = !this.editMode && !this.prefillMode && this.row.courseIsLimitApply + ? this.validateCourseTime() : Promise.resolve(true) + courseTimeValidation.then((courseTimeValid) => { + if (!courseTimeValid) return + const formData = clone(this.formData) + const array = formData.mobileColumnsValue.map((item) => { + return item.map((column) => { + return { + columnName: column.columnName, + columnValue: column.columnValue, + columnCode: column.columnCode, + columnFormType: column.columnFormType + } + }) }) + formData.mobileColumnsValue = JSON.stringify(clone(array)) + const url = this.prefillMode + ? "/platform/family/apply/savePrefill" + : (this.editMode + ? "/platform/family/apply/updateSignUp" + : "/platform/family/apply/doSignUp") + this.formLoading = true + this.$axios.post(url, formData) + .then((res) => { + this.$dialog.alert({title: "温馨提示", message: res.msg}) + .then(() => { + if (res.code !== 0) return + h5PopupHistory.clearOwner(this.historyOwner) + this.$emit("refresh") + }) + }) + .finally(() => { + this.formLoading = false + }) + }) }) }) }) diff --git a/src/main/resources/views/platform/zhghh5/activity/family/list/index.html b/src/main/resources/views/platform/zhghh5/activity/family/list/index.html index af23ebc..78724c3 100644 --- a/src/main/resources/views/platform/zhghh5/activity/family/list/index.html +++ b/src/main/resources/views/platform/zhghh5/activity/family/list/index.html @@ -71,10 +71,22 @@ layout("/layouts/platform_h5.html"){ 微信群二维码