From c909f0a3fb7f152903253fddfb2afc2d5edbef9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E8=AF=9A?= <1009578407@qq.com> Date: Wed, 30 Sep 2026 15:35:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=9C=BA=E5=9C=B0=E9=A2=84=E7=BA=A6-?= =?UTF-8?q?=E9=9D=A2=E5=90=91=E5=88=86=E5=B7=A5=E4=BC=9A=E5=9C=BA=E5=9C=B0?= =?UTF-8?q?=EF=BC=8C1=E3=80=81=E6=94=AF=E6=8C=81=E5=8D=8F=E4=BC=9A?= =?UTF-8?q?=E8=B4=9F=E8=B4=A3=E4=BA=BA=E8=BF=9B=E8=A1=8C=E9=A2=84=E7=BA=A6?= =?UTF-8?q?=EF=BC=8C2=E3=80=81=E9=A2=84=E7=BA=A6=E5=89=8D=E7=AD=BE?= =?UTF-8?q?=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../site/SiteCommitmentController.java | 58 +++++ .../controller/site/SiteManageController.java | 43 ++++ .../site/SiteReserveController.java | 79 +++++-- .../models/ActivitySiteCommitment.java | 60 +++++ .../activity/models/ActivitySiteInfo.java | 10 + .../activity/services/SiteReserveService.java | 42 ++++ .../services/impl/SiteReserveServiceImpl.java | 145 ++++++++++++ .../site/SiteInfoMobileController.java | 28 ++- .../upgrade/site_reservation_commitment.sql | 38 +++ .../components/activity/site/ReserveInfo.vue | 14 ++ .../activity/site/SiteCommitment.vue | 219 ++++++++++++++++++ .../views/mobile/activity/site/audit.html | 8 +- .../views/mobile/activity/site/info.html | 4 +- .../views/mobile/activity/site/my.html | 66 +++++- .../views/mobile/activity/site/reserve.html | 207 +++++++++++++++-- .../platform/activity/site/SiteManage.html | 26 +++ .../platform/activity/site/SiteReserve.html | 73 ++++-- 17 files changed, 1052 insertions(+), 68 deletions(-) create mode 100644 src/main/java/io/v/nutz/activity/controller/site/SiteCommitmentController.java create mode 100644 src/main/java/io/v/nutz/activity/models/ActivitySiteCommitment.java create mode 100644 src/main/resources/db/upgrade/site_reservation_commitment.sql create mode 100644 src/main/resources/static/components/activity/site/SiteCommitment.vue diff --git a/src/main/java/io/v/nutz/activity/controller/site/SiteCommitmentController.java b/src/main/java/io/v/nutz/activity/controller/site/SiteCommitmentController.java new file mode 100644 index 0000000..1f5c9e5 --- /dev/null +++ b/src/main/java/io/v/nutz/activity/controller/site/SiteCommitmentController.java @@ -0,0 +1,58 @@ +package io.v.nutz.activity.controller.site; + +import cn.wizzer.framework.base.Result; +import io.v.nutz.activity.services.SiteReserveService; +import io.v.nutz.annontation.ViReturn; +import org.apache.shiro.authz.annotation.RequiresAuthentication; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.POST; + +/** + * 电脑端和手机端共用的场地预约承诺书阅读、签署入口。 + */ +@IocBean +@At("/platform/activity/site/commitment") +@Ok("json:full") +@RequiresAuthentication +public class SiteCommitmentController { + + @Inject + private SiteReserveService siteReserveService; + + /** + * 校验预约资格并记录服务器阅读开始时间。 + * @param siteId 场地ID + * @return 承诺书及阅读凭证,或不可预约原因 + */ + @At + @POST + @ViReturn + public Object begin(String siteId) { + try { + return siteReserveService.beginCommitment(siteId); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + /** + * 阅读十秒后签署本次承诺书。 + * @param commitmentId 本次阅读凭证 + * @param signature 本次手写签名PNG数据 + * @return 签署结果 + */ + @At + @POST + @ViReturn + public Object sign(String commitmentId, String signature) { + try { + siteReserveService.signCommitment(commitmentId, signature); + return Result.success("签署成功"); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } +} diff --git a/src/main/java/io/v/nutz/activity/controller/site/SiteManageController.java b/src/main/java/io/v/nutz/activity/controller/site/SiteManageController.java index 97500ba..20211f7 100644 --- a/src/main/java/io/v/nutz/activity/controller/site/SiteManageController.java +++ b/src/main/java/io/v/nutz/activity/controller/site/SiteManageController.java @@ -2,6 +2,10 @@ package io.v.nutz.activity.controller.site; import io.v.nutz.activity.models.ActivitySiteInfo; +import cn.wizzer.framework.base.Result; +import org.jsoup.Jsoup; +import org.jsoup.safety.Whitelist; +import java.nio.charset.StandardCharsets; import io.v.nutz.activity.services.SiteInfoService; import io.v.nutz.annontation.ViReturn; import io.v.nutz.base.utils.PageUtil; @@ -62,16 +66,34 @@ public class SiteManageController { return siteInfoService.listPage(pageNumber, pageSize, cnd); } + /** + * 新增场地,面向分工会时要求配置预约前承诺书。 + * @param activitySiteInfo 场地信息及承诺书标题、正文 + * @return 保存结果或配置校验错误 + */ @At @ViReturn public Object doAdd(@Param("data") ActivitySiteInfo activitySiteInfo) { + String error = prepareCommitment(activitySiteInfo); + if (error != null) { + return Result.error(error); + } siteInfoService.insert(activitySiteInfo); return null; } + /** + * 保存场地及承诺书配置,已签署申请继续保留原正文快照。 + * @param activitySiteInfo 修改后的场地信息 + * @return 保存结果或配置校验错误 + */ @At @ViReturn public Object doEdit(@Param("data") ActivitySiteInfo activitySiteInfo) { + String error = prepareCommitment(activitySiteInfo); + if (error != null) { + return Result.error(error); + } siteInfoService.update(activitySiteInfo); return null; } @@ -88,4 +110,25 @@ public class SiteManageController { public Object findOneSite(String id) { return siteInfoService.fetch(id); } + + // 承诺书会在预约弹窗和历史签署记录中展示,仅保留正文所需的安全HTML。 + private String prepareCommitment(ActivitySiteInfo site) { + if (site.getCommitmentContent() != null) { + site.setCommitmentContent(Jsoup.clean(site.getCommitmentContent(), Whitelist.relaxed())); + } + if (Integer.valueOf(1).equals(site.getReserveTarget())) { + if (Strings.isBlank(site.getCommitmentTitle()) || Strings.isBlank(site.getCommitmentContent()) + || Strings.isBlank(Jsoup.parse(site.getCommitmentContent()).text())) { + return "面向分工会的场地必须配置承诺书标题和正文"; + } + } + if (site.getCommitmentTitle() != null && site.getCommitmentTitle().length() > 200) { + return "承诺书标题不能超过200字"; + } + if (site.getCommitmentContent() != null + && site.getCommitmentContent().getBytes(StandardCharsets.UTF_8).length > 60000) { + return "承诺书正文过长,请精简内容"; + } + return null; + } } diff --git a/src/main/java/io/v/nutz/activity/controller/site/SiteReserveController.java b/src/main/java/io/v/nutz/activity/controller/site/SiteReserveController.java index 2926c44..3688262 100644 --- a/src/main/java/io/v/nutz/activity/controller/site/SiteReserveController.java +++ b/src/main/java/io/v/nutz/activity/controller/site/SiteReserveController.java @@ -41,6 +41,7 @@ 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.trans.Trans; import java.util.ArrayList; import java.util.Date; @@ -161,9 +162,25 @@ public class SiteReserveController { return fetch.getReserve_state() > stateCode; } + /** + * 创建预约,面向分工会的场地必须先完成本次承诺书签署。 + * @param activitySiteReserve 预约表单 + * @param days 本次申请的预约日期 + * @param commitmentId 当前用户签署的场地承诺书凭证 + * @return 预约结果或资格、签署校验失败原因 + */ @At @ViReturn - public Object doAdd(@Param("data") ActivitySiteReserve activitySiteReserve, @Param("days") String[] days) { + public Object doAdd(@Param("data") ActivitySiteReserve activitySiteReserve, @Param("days") String[] days, String commitmentId) { + + if (activitySiteReserve == null || days == null || days.length == 0) { + return Result.error("请选择预约场地和日期"); + } + try { + siteReserveService.checkCommitment(activitySiteReserve.getSite_id(), commitmentId); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } Map validateSiteReserve = siteReserveService.validateSiteReserve( activitySiteReserve.getSite_id(), @@ -183,22 +200,31 @@ public class SiteReserveController { Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName); if (stateCode == null) { - return null; + return Result.error("场地尚未配置预约审核流程"); } - for (String d : days) { - activitySiteReserve.setReserve_day(d); - //查询这个人是否已经预约这个时间段 - int count = siteReserveService.count("activity_site_reserve", Cnd.NEW().and("reserve_person_id", "=", ShiroUtil.getPrincipalProperty("id")) - .and("reserve_day", "=", activitySiteReserve.getReserve_day()).and("start_time", "=", activitySiteReserve.getStart_time()) - .and("end_time", "=", activitySiteReserve.getEnd_time())); - if (count > 0) { - return Result.error("您已预约该时间段!"); - } + try { + // 同一申请的多天预约和签署凭证一起提交,失败时不消耗签署凭证。 + Trans.exec(() -> { + siteReserveService.bindCommitment(activitySiteReserve.getSite_id(), commitmentId, r); + for (String d : days) { + activitySiteReserve.setReserve_day(d); + //查询这个人是否已经预约这个时间段 + int count = siteReserveService.count("activity_site_reserve", Cnd.NEW().and("reserve_person_id", "=", ShiroUtil.getPrincipalProperty("id")) + .and("reserve_day", "=", activitySiteReserve.getReserve_day()).and("start_time", "=", activitySiteReserve.getStart_time()) + .and("end_time", "=", activitySiteReserve.getEnd_time())); + if (count > 0) { + throw new IllegalArgumentException("您已预约该时间段!"); + } - activitySiteReserve.setReserve_state(stateCode); - activitySiteReserve.setReserve_person_id(ShiroUtil.getPrincipalProperty("id").toString()); - siteReserveService.insert(activitySiteReserve); + activitySiteReserve.setReserve_state(stateCode); + activitySiteReserve.setReserve_person_id(ShiroUtil.getPrincipalProperty("id").toString()); + activitySiteReserve.setId(R.UU32()); + siteReserveService.insert(activitySiteReserve); + } + }); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); } //发送微信提醒审核人,查询状态码对应的审核人 @@ -307,10 +333,18 @@ public class SiteReserveController { return siteReserveService.count(cnd); } + /** + * 查询预约详情及本次申请签署的承诺书快照。 + * @param id 预约记录ID + * @return 预约详情、审核记录和承诺书签署信息 + */ @At @ViReturn public Object findOne(String id) { ActivitySiteReserve fetch = siteReserveService.fetch(id); + if (fetch == null) { + return Result.error("预约记录不存在"); + } Sql sql = Sqls.create(""" SELECT asr.*, @@ -386,6 +420,7 @@ public class SiteReserveController { i.setCondition(cnd); List list1 = siteReserveService.list(i); record.set("site_info", list1); + record.set("commitment", siteReserveService.findCommitment(fetch.getSqid())); return record; } @@ -455,6 +490,13 @@ public class SiteReserveController { return result; } + /** + * 获取指定日期的场地预约时段,支持以24:00作为营业结束时间。 + * + * @param siteId 场地ID + * @param day 预约日期,格式为yyyy-MM-dd + * @return 预约时段及不可预约状态,保留配置的结束时间用于匹配预约记录 + */ @At @ViReturn public Object getTimesByDay(String siteId, String day) { @@ -480,16 +522,19 @@ public class SiteReserveController { } else { // 构造起始时间 DateTime start = DateUtil.parse(startTime, "HH:mm"); // "09:00" - DateTime end = DateUtil.parse(endTime, "HH:mm"); // "17:00" + // 场地允许营业至24:00,按次日零点计算,返回时保留24:00以匹配预约记录。 + DateTime end = "24:00".equals(endTime) + ? DateUtil.offsetDay(DateUtil.parse("00:00", "HH:mm"), 1) + : DateUtil.parse(endTime, "HH:mm"); DateTime current = start; while (DateUtil.compare(current, end) < 0) { // 当前时间 <= 结束时间 DateTime next = DateUtil.offsetSecond(current, (int) (timeUnit * 3600)); // 如果超过结束时间,则使用 endTime if (DateUtil.compare(next, end) > 0) { - result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", DateUtil.format(end, "HH:mm"))); + result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", endTime)); break; } else { - result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", DateUtil.format(next, "HH:mm"))); + result.add(NutMap.NEW().addv("start_time", DateUtil.format(current, "HH:mm")).addv("end_time", DateUtil.compare(next, end) == 0 ? endTime : DateUtil.format(next, "HH:mm"))); } current = next; } diff --git a/src/main/java/io/v/nutz/activity/models/ActivitySiteCommitment.java b/src/main/java/io/v/nutz/activity/models/ActivitySiteCommitment.java new file mode 100644 index 0000000..59e21ad --- /dev/null +++ b/src/main/java/io/v/nutz/activity/models/ActivitySiteCommitment.java @@ -0,0 +1,60 @@ +package io.v.nutz.activity.models; + +import lombok.Data; +import org.nutz.dao.entity.annotation.*; + +/** + * 场地预约承诺书阅读、签署及申请关联记录,保留签署时的正文快照。 + */ +@Data +@Table("activity_site_commitment") +public class ActivitySiteCommitment { + + @Name + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + private String id; + + @Column + @Comment("场地ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String siteId; + + @Column + @Comment("签署人ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String userId; + + @Column + @Comment("签署人姓名快照") + @ColDefine(type = ColType.VARCHAR, width = 120) + private String userName; + + @Column + @Comment("承诺书标题快照") + @ColDefine(type = ColType.VARCHAR, width = 200) + private String title; + + @Column + @Comment("承诺书正文快照") + @ColDefine(type = ColType.TEXT) + private String content; + + @Column + @Comment("服务器记录的阅读开始时间,毫秒") + private Long readAt; + + @Column + @Comment("服务器记录的签署时间,毫秒") + private Long signedAt; + + @Column + @Comment("本次手写签名PNG数据") + @ColDefine(type = ColType.TEXT) + private String signature; + + @Column + @Comment("预约申请编号,未提交时为空") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String sqid; +} diff --git a/src/main/java/io/v/nutz/activity/models/ActivitySiteInfo.java b/src/main/java/io/v/nutz/activity/models/ActivitySiteInfo.java index 33e5607..603d64a 100644 --- a/src/main/java/io/v/nutz/activity/models/ActivitySiteInfo.java +++ b/src/main/java/io/v/nutz/activity/models/ActivitySiteInfo.java @@ -83,6 +83,16 @@ public class ActivitySiteInfo { @ColDefine(type = ColType.TEXT) private String introduce; + @Column + @Comment("预约前承诺书标题") + @ColDefine(type = ColType.VARCHAR, width = 200) + private String commitmentTitle; + + @Column + @Comment("预约前承诺书正文") + @ColDefine(type = ColType.TEXT) + private String commitmentContent; + @Column @Comment("可容纳人数") @ColDefine(type = ColType.INT) diff --git a/src/main/java/io/v/nutz/activity/services/SiteReserveService.java b/src/main/java/io/v/nutz/activity/services/SiteReserveService.java index 0b5847f..94e6339 100644 --- a/src/main/java/io/v/nutz/activity/services/SiteReserveService.java +++ b/src/main/java/io/v/nutz/activity/services/SiteReserveService.java @@ -2,11 +2,53 @@ package io.v.nutz.activity.services; import io.v.nutz.activity.models.ActivitySiteReserve; +import io.v.nutz.activity.models.ActivitySiteCommitment; import io.v.nutz.base.service.BaseService; +import org.nutz.lang.util.NutMap; import java.util.Map; public interface SiteReserveService extends BaseService { Map validateSiteReserve(String siteId, String day, String startTime, String endTime, Integer joinCount); + + /** + * 校验当前用户的预约资格并开始本次承诺书阅读。 + * @param siteId 预约场地ID + * @return 是否需要签署及本次承诺书快照、凭证 + * @throws IllegalArgumentException 无预约资格或场地未配置承诺书 + */ + NutMap beginCommitment(String siteId); + + /** + * 阅读满十秒后保存当前用户本次手写签名。 + * @param commitmentId 本次阅读凭证 + * @param signature PNG格式的签名数据 + * @throws IllegalArgumentException 阅读不足十秒、凭证失效或签名无效 + */ + void signCommitment(String commitmentId, String signature); + + /** + * 预约提交前校验身份、承诺书版本和本次签署状态。 + * @param siteId 预约场地ID + * @param commitmentId 签署凭证,非分工会场地可为空 + * @throws IllegalArgumentException 预约资格或签署条件不满足 + */ + void checkCommitment(String siteId, String commitmentId); + + /** + * 在预约保存事务中一次性关联签署凭证,失败时随预约一起回滚。 + * @param siteId 预约场地ID + * @param commitmentId 签署凭证 + * @param sqid 本次预约申请编号 + * @throws IllegalArgumentException 凭证失效或已被其他申请使用 + */ + void bindCommitment(String siteId, String commitmentId, String sqid); + + /** + * 查询申请签署时的承诺书,供已有预约详情展示。 + * @param sqid 预约申请编号 + * @return 签署快照,历史未签署申请返回null + */ + ActivitySiteCommitment findCommitment(String sqid); } diff --git a/src/main/java/io/v/nutz/activity/services/impl/SiteReserveServiceImpl.java b/src/main/java/io/v/nutz/activity/services/impl/SiteReserveServiceImpl.java index 9690b46..1584465 100644 --- a/src/main/java/io/v/nutz/activity/services/impl/SiteReserveServiceImpl.java +++ b/src/main/java/io/v/nutz/activity/services/impl/SiteReserveServiceImpl.java @@ -2,6 +2,7 @@ package io.v.nutz.activity.services.impl; import io.v.nutz.activity.models.ActivitySiteInfo; +import io.v.nutz.activity.models.ActivitySiteCommitment; import io.v.nutz.activity.models.ActivitySiteReserve; import io.v.nutz.activity.models.ActivityType; import io.v.nutz.activity.services.SiteReserveService; @@ -9,8 +10,20 @@ import io.v.nutz.base.service.impl.BaseServiceImpl; import io.v.nutz.constant.AuditTypeEnum; import io.v.nutz.modules.models.AuditState; import org.nutz.dao.Cnd; +import org.nutz.dao.Chain; import org.nutz.dao.Dao; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Strings; +import org.nutz.lang.random.R; +import org.nutz.lang.util.NutMap; +import org.apache.shiro.SecurityUtils; +import io.v.nutz.sys.models.Sys_user; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.util.Base64; +import java.util.Objects; import java.util.List; import java.util.Map; @@ -28,6 +41,138 @@ public class SiteReserveServiceImpl extends BaseServiceImpl super(dao); } + private ActivitySiteInfo commitmentSite(String siteId) { + ActivitySiteInfo site = Strings.isBlank(siteId) ? null : dao().fetch(ActivitySiteInfo.class, siteId); + if (site == null || !Boolean.TRUE.equals(site.getState())) { + throw new IllegalArgumentException("场地不存在或已关闭预约"); + } + if (!SecurityUtils.getSubject().isAuthenticated()) { + throw new IllegalArgumentException("请先登录后预约"); + } + if (Integer.valueOf(1).equals(site.getReserveTarget()) + && !SecurityUtils.getSubject().hasRole("sysadmin") + && !SecurityUtils.getSubject().hasRole("gh01") + && !SecurityUtils.getSubject().hasRole("H04") + && !SecurityUtils.getSubject().hasRole("H02")) { + throw new IllegalArgumentException("该场地仅限分工会预约人员或协会负责人预约"); + } + return site; + } + + private ActivitySiteCommitment currentCommitment(String commitmentId) { + ActivitySiteCommitment record = Strings.isBlank(commitmentId) ? null + : dao().fetch(ActivitySiteCommitment.class, commitmentId); + Sys_user user = (Sys_user) SecurityUtils.getSubject().getPrincipal(); + if (user == null || record == null || !user.getId().equals(record.getUserId()) + || record.getReadAt() == null || System.currentTimeMillis() - record.getReadAt() > 30 * 60 * 1000L + || Strings.isNotBlank(record.getSqid())) { + throw new IllegalArgumentException("签署凭证已失效,请重新阅读并签署承诺书"); + } + ActivitySiteInfo site = commitmentSite(record.getSiteId()); + if (!Objects.equals(site.getCommitmentTitle(), record.getTitle()) + || !Objects.equals(site.getCommitmentContent(), record.getContent())) { + throw new IllegalArgumentException("承诺书内容已更新,请重新阅读并签署"); + } + return record; + } + + /** {@inheritDoc} */ + @Override + public NutMap beginCommitment(String siteId) { + ActivitySiteInfo site = commitmentSite(siteId); + if (!Integer.valueOf(1).equals(site.getReserveTarget())) { + return NutMap.NEW().addv("required", false); + } + if (Strings.isBlank(site.getCommitmentTitle()) || Strings.isBlank(site.getCommitmentContent())) { + throw new IllegalArgumentException("场地尚未配置承诺书,暂不能预约,请联系场地管理员"); + } + Sys_user user = (Sys_user) SecurityUtils.getSubject().getPrincipal(); + ActivitySiteCommitment record = new ActivitySiteCommitment(); + record.setId(R.UU32()); + record.setSiteId(siteId); + record.setUserId(user.getId()); + record.setUserName(user.getUsername()); + record.setTitle(site.getCommitmentTitle()); + record.setContent(site.getCommitmentContent()); + record.setReadAt(System.currentTimeMillis()); + dao().insert(record); + return NutMap.NEW().addv("required", true).addv("record", record).addv("seconds", 10); + } + + /** {@inheritDoc} */ + @Override + public void signCommitment(String commitmentId, String signature) { + ActivitySiteCommitment record = currentCommitment(commitmentId); + if (System.currentTimeMillis() - record.getReadAt() < 10000) { + throw new IllegalArgumentException("请阅读承诺书满10秒后再签字确认"); + } + validateSignature(signature); + int changed = dao().update(ActivitySiteCommitment.class, + Chain.make("signature", signature).add("signedAt", System.currentTimeMillis()), + Cnd.where("id", "=", record.getId()).and("signedAt", "is", null).and("sqid", "is", null)); + if (changed != 1) { + throw new IllegalArgumentException("本次签署已完成,请勿重复提交"); + } + } + + // 只接受有实际笔迹的PNG,避免空白画布被当成已签署承诺书。 + private void validateSignature(String signature) { + if (signature == null || signature.length() > 60000 || !signature.startsWith("data:image/png;base64,")) { + throw new IllegalArgumentException("请填写有效的手写签名"); + } + try { + byte[] bytes = Base64.getDecoder().decode(signature.substring("data:image/png;base64,".length())); + BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes)); + if (image != null && image.getWidth() <= 1600 && image.getHeight() <= 800) { + int ink = 0; + for (int y = 0; y < image.getHeight(); y++) { + for (int x = 0; x < image.getWidth(); x++) { + int pixel = image.getRGB(x, y); + if ((pixel >>> 24) > 128 && (pixel & 0x00ffffff) < 0xdddddd && ++ink >= 20) { + return; + } + } + } + } + } catch (Exception e) { + throw new IllegalArgumentException("签名数据无效,请重新签字"); + } + throw new IllegalArgumentException("签名不能为空,请在签字区域手写签名"); + } + + /** {@inheritDoc} */ + @Override + public void checkCommitment(String siteId, String commitmentId) { + ActivitySiteInfo site = commitmentSite(siteId); + if (Integer.valueOf(1).equals(site.getReserveTarget())) { + ActivitySiteCommitment record = currentCommitment(commitmentId); + if (!siteId.equals(record.getSiteId()) || record.getSignedAt() == null + || record.getSignedAt() - record.getReadAt() < 10000 || Strings.isBlank(record.getSignature())) { + throw new IllegalArgumentException("请先阅读并签署当前场地的承诺书"); + } + } + } + + /** {@inheritDoc} */ + @Override + public void bindCommitment(String siteId, String commitmentId, String sqid) { + checkCommitment(siteId, commitmentId); + ActivitySiteInfo site = dao().fetch(ActivitySiteInfo.class, siteId); + if (Integer.valueOf(1).equals(site.getReserveTarget())) { + int changed = dao().update(ActivitySiteCommitment.class, Chain.make("sqid", sqid), + Cnd.where("id", "=", commitmentId).and("sqid", "is", null)); + if (changed != 1) { + throw new IllegalArgumentException("签署凭证已用于其他申请,请重新签署"); + } + } + } + + /** {@inheritDoc} */ + @Override + public ActivitySiteCommitment findCommitment(String sqid) { + return Strings.isBlank(sqid) ? null : dao().fetch(ActivitySiteCommitment.class, Cnd.where("sqid", "=", sqid)); + } + @Override public Map validateSiteReserve(String siteId, String day, String startTime, String endTime, Integer joinCount) { ActivitySiteInfo siteInfo = dao().fetch(ActivitySiteInfo.class, siteId); diff --git a/src/main/java/io/v/nutz/mobile/activity/site/SiteInfoMobileController.java b/src/main/java/io/v/nutz/mobile/activity/site/SiteInfoMobileController.java index 450de62..73d8ebf 100644 --- a/src/main/java/io/v/nutz/mobile/activity/site/SiteInfoMobileController.java +++ b/src/main/java/io/v/nutz/mobile/activity/site/SiteInfoMobileController.java @@ -31,6 +31,7 @@ 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.trans.Trans; import java.util.ArrayList; import java.util.List; @@ -234,10 +235,26 @@ public class SiteInfoMobileController { return hours; } + /** + * 手机端预约入口,使用与电脑端相同的预约资格和承诺书校验。 + * @param siteId 场地ID + * @param times 预约日期及时段JSON + * @param message 预约事由 + * @param joinUser 参与人员 + * @param joinCount 预约人数 + * @param commitmentId 当前用户的本次承诺书签署凭证 + * @return 预约结果或签署校验失败原因 + */ @At("/reserveDo") @Ok("json:full") @ViReturn - public Object reserveDo(String siteId, String times, String message, String joinUser, Integer joinCount) { + public Object reserveDo(String siteId, String times, String message, String joinUser, Integer joinCount, String commitmentId) { + + try { + siteReserveService.checkCommitment(siteId, commitmentId); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } String moduleName = activityCommonService.findModuleNameByActivityId(siteId); Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName); @@ -272,7 +289,14 @@ public class SiteInfoMobileController { } str = str.substring(0, str.length() - 1); - siteReserveService.insert(li); + try { + Trans.exec(() -> { + siteReserveService.bindCommitment(siteId, commitmentId, r); + siteReserveService.insert(li); + }); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } //发送微信提醒审核人 //查询状态码对应的审核人 diff --git a/src/main/resources/db/upgrade/site_reservation_commitment.sql b/src/main/resources/db/upgrade/site_reservation_commitment.sql new file mode 100644 index 0000000..761d44b --- /dev/null +++ b/src/main/resources/db/upgrade/site_reservation_commitment.sql @@ -0,0 +1,38 @@ +-- 场地预约承诺书:在已选定的业务数据库中执行,不包含历史数据修改。 +-- 项目启动时可能已通过实体自动建表、补列,因此跳过已存在的表和字段。 +CREATE TABLE IF NOT EXISTS `activity_site_commitment` ( + `id` VARCHAR(32) NOT NULL, + `siteId` VARCHAR(32) DEFAULT NULL COMMENT '场地ID', + `userId` VARCHAR(32) DEFAULT NULL COMMENT '签署人ID', + `userName` VARCHAR(120) DEFAULT NULL COMMENT '签署人姓名快照', + `title` VARCHAR(200) DEFAULT NULL COMMENT '承诺书标题快照', + `content` TEXT COMMENT '承诺书正文快照', + `readAt` BIGINT DEFAULT NULL COMMENT '服务器记录的阅读开始时间,毫秒', + `signedAt` BIGINT DEFAULT NULL COMMENT '服务器记录的签署时间,毫秒', + `signature` TEXT COMMENT '本次手写签名PNG数据', + `sqid` VARCHAR(32) DEFAULT NULL COMMENT '预约申请编号,未提交时为空', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场地预约承诺书签署记录'; + +-- 仅补充缺失的场地配置列,保留已经配置的承诺书及已有字段定义。 +SET @site_commitment_ddl = IF( + EXISTS(SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activity_site_info' + AND COLUMN_NAME = 'commitmentTitle'), + 'SELECT ''commitmentTitle already exists'' AS message', + 'ALTER TABLE `activity_site_info` ADD COLUMN `commitmentTitle` VARCHAR(200) DEFAULT NULL COMMENT ''预约前承诺书标题''' +); +PREPARE site_commitment_stmt FROM @site_commitment_ddl; +EXECUTE site_commitment_stmt; +DEALLOCATE PREPARE site_commitment_stmt; + +SET @site_commitment_ddl = IF( + EXISTS(SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activity_site_info' + AND COLUMN_NAME = 'commitmentContent'), + 'SELECT ''commitmentContent already exists'' AS message', + 'ALTER TABLE `activity_site_info` ADD COLUMN `commitmentContent` TEXT COMMENT ''预约前承诺书正文''' +); +PREPARE site_commitment_stmt FROM @site_commitment_ddl; +EXECUTE site_commitment_stmt; +DEALLOCATE PREPARE site_commitment_stmt; diff --git a/src/main/resources/static/components/activity/site/ReserveInfo.vue b/src/main/resources/static/components/activity/site/ReserveInfo.vue index a62d05d..3291919 100644 --- a/src/main/resources/static/components/activity/site/ReserveInfo.vue +++ b/src/main/resources/static/components/activity/site/ReserveInfo.vue @@ -56,6 +56,17 @@ + + +

本次预约无承诺书签署记录

+
+ @@ -166,6 +177,9 @@ module.exports = { } }, methods: { + commitmentTime(value) { + return moment(value).format('YYYY-MM-DD HH:mm:ss') + }, hasPane(name) { if (!this.handle) { return true diff --git a/src/main/resources/static/components/activity/site/SiteCommitment.vue b/src/main/resources/static/components/activity/site/SiteCommitment.vue new file mode 100644 index 0000000..c0c56f8 --- /dev/null +++ b/src/main/resources/static/components/activity/site/SiteCommitment.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/src/main/resources/views/mobile/activity/site/audit.html b/src/main/resources/views/mobile/activity/site/audit.html index a8d511b..f176cb2 100644 --- a/src/main/resources/views/mobile/activity/site/audit.html +++ b/src/main/resources/views/mobile/activity/site/audit.html @@ -185,6 +185,7 @@ layout("/mobile/platform.html"){
+ @@ -357,6 +358,8 @@ layout("/mobile/platform.html"){ + 查看承诺书
\ No newline at end of file +#--> diff --git a/src/main/resources/views/mobile/activity/site/info.html b/src/main/resources/views/mobile/activity/site/info.html index 041326b..dfc641b 100644 --- a/src/main/resources/views/mobile/activity/site/info.html +++ b/src/main/resources/views/mobile/activity/site/info.html @@ -291,8 +291,8 @@ layout("/mobile/platform.html"){ this.show = true }, openView(o) { - if(o.reservetarget === 1 && "${@shiro.hasAnyRoles('gh01', 'H04')}" === 'false' && "${@shiro.hasRole('sysadmin')}" === 'false') { - vant.Toast('该场地只能分工会主席预约') + if(o.reservetarget === 1 && "${@shiro.hasAnyRoles('gh01', 'H04', 'H02')}" === 'false' && "${@shiro.hasRole('sysadmin')}" === 'false') { + vant.Toast('该场地仅限分工会预约人员或协会负责人预约') return } this.siteId = o.id diff --git a/src/main/resources/views/mobile/activity/site/my.html b/src/main/resources/views/mobile/activity/site/my.html index b9cc575..ce49256 100644 --- a/src/main/resources/views/mobile/activity/site/my.html +++ b/src/main/resources/views/mobile/activity/site/my.html @@ -155,12 +155,16 @@ layout("/mobile/platform.html"){ right: 0; top: 10px; } + .reservation-commitment-content img { + max-width: 100%; + } .van-cell__title { font-size: 16px; }
+ @@ -217,7 +221,7 @@ layout("/mobile/platform.html"){ @load="onLoad">
-
+
@@ -274,6 +278,12 @@ layout("/mobile/platform.html"){ 反馈意见 + 预约详情 + 查看承诺书
@@ -288,6 +298,32 @@ layout("/mobile/platform.html"){
+ +
+

正在加载预约信息…

+

{{detailError}}

+ +
+
+ \ No newline at end of file +#--> diff --git a/src/main/resources/views/mobile/activity/site/reserve.html b/src/main/resources/views/mobile/activity/site/reserve.html index 45b8286..856c8bc 100644 --- a/src/main/resources/views/mobile/activity/site/reserve.html +++ b/src/main/resources/views/mobile/activity/site/reserve.html @@ -3,6 +3,112 @@ layout("/mobile/platform.html"){ #--> -
+
+ +
+ +
+
+ +

活动场地预约

+

选择合适的时间,为活动做好准备

+
+
+
+
1阅读须知
+ +
2选择时段
+ +
3提交预约
+
+

如场地要求签署承诺书,请阅读满10秒并完成手写签名,再选择预约日期和时间。

+ 进入场地预约 +
+ 返回场地列表 +
+
+
diff --git a/src/main/resources/views/platform/activity/site/SiteManage.html b/src/main/resources/views/platform/activity/site/SiteManage.html index 4aeb0df..4649443 100644 --- a/src/main/resources/views/platform/activity/site/SiteManage.html +++ b/src/main/resources/views/platform/activity/site/SiteManage.html @@ -193,6 +193,19 @@ layout("/layouts/platform.html"){ +