diff --git a/src/main/java/io/v/nutz/zhgh/activity/controller/site/ActivityTypeController.java b/src/main/java/io/v/nutz/zhgh/activity/controller/site/ActivityTypeController.java index e639853..9329337 100644 --- a/src/main/java/io/v/nutz/zhgh/activity/controller/site/ActivityTypeController.java +++ b/src/main/java/io/v/nutz/zhgh/activity/controller/site/ActivityTypeController.java @@ -1,164 +1,50 @@ package io.v.nutz.zhgh.activity.controller.site; - -import cn.wizzer.framework.base.Result; import io.v.nutz.zhgh.activity.models.ActivityType; -import io.v.nutz.zhgh.activity.services.ActivityCommonService; import io.v.nutz.zhgh.activity.services.ActivityTypeService; import io.v.nutz.base.annontation.ViReturn; -import io.v.nutz.base.model.AuditState; -import io.v.nutz.base.model.AuditStateUser; -import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.apache.shiro.authz.annotation.*; import org.nutz.dao.Cnd; -import org.nutz.dao.Dao; -import org.nutz.dao.Sqls; -import org.nutz.dao.sql.Sql; -import org.nutz.dao.util.Daos; -import org.nutz.dao.util.cri.SqlExpressionGroup; -import org.nutz.ioc.loader.annotation.Inject; -import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.ioc.loader.annotation.*; +import org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.json.Json; -import org.nutz.mvc.annotation.At; -import org.nutz.mvc.annotation.Ok; -import org.nutz.mvc.annotation.Param; +import org.nutz.mvc.annotation.*; -import javax.servlet.http.HttpServletRequest; -import java.util.List; - -/** - * @Author JyuHsin - * @Date 2022/5/11 - * @Description - */ @IocBean @At("/platform/activity/site/type") +@Ok("json:full") public class ActivityTypeController { - - @Inject - private Dao dao; - - @Inject - private ActivityTypeService activityTypeService; - - @Inject - private ActivityCommonService activityCommonService; - - @At("") - @Ok("beetl:/platform/activity/site/SiteType.html") - @RequiresPermissions("activity.site.type") - public void index(HttpServletRequest request) { - + @Inject private ActivityTypeService activityTypeService; + @At("") @Ok("beetl:/platform/activity/site/SiteType.html") @RequiresPermissions("activity.site.type") + public void index() { } + /** enabledOnly=true 仅返回可预约类型;默认返回全部供历史筛选,结果为类型数组。 */ + @At @ViReturn @RequiresAuthentication + public Object findAll(Boolean enabledOnly) { + Cnd c=Cnd.NEW(); c.asc("sortNum"); + if(Boolean.TRUE.equals(enabledOnly)) c.and("enabled","=",true); + return activityTypeService.query(c); } - - @At("/findAll") - @Ok("json:full") - @ViReturn - public Object findAll() { - List meetingTypeList = dao.query(ActivityType.class, Cnd.NEW().asc("sortNum")); - dao.fetchLinks(meetingTypeList, "^auditStateList$"); - for (ActivityType ActivityType : meetingTypeList) { - List auditStateList = ActivityType.getAuditStateList(); - dao.fetchLinks(auditStateList, "^auditStateUserList$"); - } - return meetingTypeList; - } - - @At - @Ok("json:full") - @ViReturn - @RequiresPermissions("activity.site.type") - public Object findOne(String module) { - - List auditStateList = dao.query(AuditState.class, Cnd.where("module", "=", module)); - auditStateList.forEach(v -> { - v.setAuditStateUserList(dao.query(AuditStateUser.class, Cnd.where("stateId", "=", v.getStateId()))); - }); - return auditStateList; - } - - @At("/doHandle") - @Ok("json:full") - @ViReturn - @RequiresPermissions("activity.site.type") - public Object doHandle(@Param("data") String data) { - ActivityType activityType = Json.fromJson(ActivityType.class, data); - - if (null == activityType.getId()) { - int count = dao.count(AuditState.class, Cnd.where("module", "=", activityType.getModuleName())); - if (count > 0) { - return Result.error("模块名称已存在,换一个试试"); - } - activityTypeService.add(activityType); - } else { - activityTypeService.edit(activityType); - } + /** pageNumber/pageSize 为分页参数,返回 list/totalCount;复用页面表格 mixin。 */ + @At @ViReturn @RequiresPermissions("activity.site.type") + public Object pageData(int pageNumber,int pageSize) { return activityTypeService.listPage(Math.max(1,pageNumber),Math.min(100,Math.max(1,pageSize)),Cnd.NEW().asc("sortNum")); } + /** data 为类型 JSON(id、code、meetingTypeName、enabled);返回 Object,由 @ViReturn 包装 code(0 成功)和 msg(结果说明)。 */ + @At @ViReturn @RequiresPermissions("activity.site.type") @Aop(TransAop.READ_COMMITTED) + public Object doHandle(String data) { + ActivityType t=Json.fromJson(ActivityType.class,data); + if(t==null) throw new IllegalArgumentException("类型参数不能为空"); + if(t.getId()==null) activityTypeService.add(t); else activityTypeService.edit(t); + // Object 返回类型使 @ViReturn 包装的 code/msg 能传递给 JSON 视图。 return null; } - - @At("/delete/?") - @Ok("json:full") - @ViReturn - @RequiresPermissions("activity.site.type") - public Object delete(Integer id) { - if (id == 1) { - return Result.error("此记录不可删除"); - } - ActivityType activityType = dao.fetch(ActivityType.class, id); - dao.clear(ActivityType.class, Cnd.where("id", "=", id)); - dao.count(AuditState.class, Cnd.where("module", "=", activityType.getModuleName())); + /** id 为类型主键,enabled 为是否启用;返回 Object,由 @ViReturn 包装 code(0 成功)和 msg(结果说明)。 */ + @At @ViReturn @RequiresPermissions("activity.site.type") @Aop(TransAop.READ_COMMITTED) + public Object setEnabled(Integer id,Boolean enabled) { + ActivityType t=activityTypeService.fetch(id); + if(t==null) throw new IllegalArgumentException("类型不存在"); + t.setEnabled(enabled); activityTypeService.edit(t); return null; } - - @At("/findUserList") - @Ok("json:full") - @ViReturn - @RequiresPermissions("activity.site.type") - public Object findUserList(@Param("keyWords") String[] keyWords) { - Sql sql = Sqls.create(""" - SELECT - id, - username, - loginname, - mobile, - sex, - unitname, - unionname, - unionid, - unitid - FROM - `user` - $condition - """); - Cnd cnd = Cnd.NEW(); - SqlExpressionGroup seg = new SqlExpressionGroup(); - for (String keyWord : keyWords) { - seg.andLike("loginname", keyWord); - seg.orLike("username", keyWord); - } - cnd.and(seg); - sql.setCondition(cnd); - return Daos.query(dao, sql.toString(), Sqls.callback.maps()); - } - - @At - @Ok("json:full") - @ViReturn - public Object findMaxStateId() { - return activityTypeService.findMaxStateId(); - } - - @At - @Ok("json:full") - @ViReturn - @RequiresPermissions("activity.site.type") - public Object checkStateId(@Param("stateId") Integer stateId, @Param("stateIndex") Integer stateIndex) { - if (stateId == null) { - return Result.error("参数错误"); - } - int count = dao.count(AuditState.class, Cnd.where("stateId", "=", stateId)); - if (count == 0) { - return Result.success().addData(stateId); - } else { - return Result.success(activityTypeService.findMaxStateId() + 10 * stateIndex); - } - } + /** id 为待删除类型主键,服务层检查场地引用;返回 Object,由 @ViReturn 包装 code(0 成功)和 msg(结果说明)。 */ + @At("/delete/?") @ViReturn @RequiresPermissions("activity.site.type") @Aop(TransAop.READ_COMMITTED) + public Object delete(Integer id) { activityTypeService.delete(activityTypeService.fetch(id)); return null; } } diff --git a/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteClubReviewController.java b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteClubReviewController.java new file mode 100644 index 0000000..6997538 --- /dev/null +++ b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteClubReviewController.java @@ -0,0 +1,42 @@ +package io.v.nutz.zhgh.activity.controller.site; +import io.v.nutz.base.annontation.ViReturn; +import io.v.nutz.zhgh.activity.services.impl.SiteBookingService; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.nutz.ioc.loader.annotation.*; +import org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.mvc.annotation.*; +import javax.servlet.http.HttpServletRequest; + +/** 协会审核:PC/H5 共享业务服务,入口固定节点,不能由请求绕过节点限制。 */ +@IocBean @Ok("json:full") @At("/platform/activity/site/review/club") +@RequiresPermissions("activity.site.review.club") +public class SiteClubReviewController { + @Inject private SiteBookingService siteBookingService; + @At("") @Ok("beetl:/platform/activity/site/SiteReview.html") + public void index(HttpServletRequest request) { + request.setAttribute("reviewTitle","协会审核"); + request.setAttribute("reviewApi","/platform/activity/site/review/club"); + } + /** isAudit 为已审核/未审核;searchName 为场地名称或预约人字段,month 为 yyyy-MM,siteType 为类型 ID,返回 list/totalCount。 */ + @At @ViReturn + public Object pageData(Boolean isAudit,String searchName,String searchKeyword,String month,String siteType,int pageNumber,int pageSize) { + return siteBookingService.reviewPage("club",Boolean.TRUE.equals(isAudit),searchName,searchKeyword,month,siteType,pageNumber,pageSize); + } + /** id 是预约记录主键,返回申请、时段及审核历史,服务层校验查看范围。 */ + @At @ViReturn + public Object detail(String id) { return siteBookingService.detail(id); } + /** ids 为预约记录 ID 数组,isPass 为通过/拒绝,auditOpinion 为必填意见;返回 Object,由 @ViReturn 包装 code(0 成功)和 msg(结果说明)。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doReview(String[] ids,Boolean isPass,String auditOpinion) { + siteBookingService.review("club",ids,isPass,auditOpinion); + // Object 返回类型保留 @ViReturn 的统一结果,避免前端收到空响应。 + return null; + } + /** id 为预约记录主键,撤回本人最近一次审核;返回统一 code/msg,成功后恢复当前节点待审。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doRevoke(String id) { + siteBookingService.revokeReview("club",id); + return null; + } +} diff --git a/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteRecordController.java b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteRecordController.java index 049d5ed..bbd4193 100644 --- a/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteRecordController.java +++ b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteRecordController.java @@ -64,7 +64,7 @@ public class SiteRecordController { LEFT JOIN activity_site_info asi ON asi.id = asr.site_id LEFT JOIN - audit_state ass ON ass.stateId = asr.reserve_state + (select state_id stateId,state_name stateName,state_color stateColor,'activity_site' module,case when state_id=4030 then 3 when state_id in (4040,4050) then 1 else 0 end stateAuditType from state where belong='activity_site') ass ON ass.stateId = asr.reserve_state LEFT JOIN sys_user u on u.id = asr.reserve_person_id $condition diff --git a/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteReserveController.java b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteReserveController.java index 4594cf7..f875e5c 100644 --- a/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteReserveController.java +++ b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteReserveController.java @@ -32,6 +32,9 @@ import org.nutz.lang.Strings; import org.nutz.lang.random.R; import org.nutz.lang.util.NutMap; import org.nutz.mvc.annotation.At; +import io.v.nutz.zhgh.activity.services.impl.SiteBookingService; +import org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; @@ -50,6 +53,8 @@ import java.util.stream.Collectors; @At("/platform/activity/site/reserve") public class SiteReserveController { + @Inject private SiteBookingService siteBookingService; + @Inject private SiteInfoService siteInfoService; @@ -101,7 +106,7 @@ public class SiteReserveController { LEFT JOIN activity_site_info asi ON asi.id = asr.site_id LEFT JOIN - audit_state ass ON ass.stateId = asr.reserve_state + (select state_id stateId,state_name stateName,state_color stateColor,'activity_site' module,case when state_id=4030 then 3 when state_id in (4040,4050) then 1 else 0 end stateAuditType from state where belong='activity_site') ass ON ass.stateId = asr.reserve_state $condition """); @@ -115,6 +120,7 @@ public class SiteReserveController { if (Strings.isNotBlank(siteId)) { cnd.and("site_id", "=", siteId); + cnd.and("reserve_state", "not in", java.util.Arrays.asList(4040,4050)); } if (Strings.isNotBlank(searchName) && Strings.isNotBlank(searchKeyword)) { @@ -140,124 +146,30 @@ public class SiteReserveController { @At @ViReturn @RequiresPermissions("activity.site.reserve") + @Aop(TransAop.READ_COMMITTED) public Object doDelete(String id, Integer op) { - ActivitySiteReserve fetch = siteReserveService.fetch(id); - if (op == 1) { - siteReserveService.clear(Cnd.NEW().and("sqid", "=", fetch.getSqid())); - } else if (op == 2) { - //siteReserveService.update(Chain.make("reserve_state", 50), Cnd.where("id", "=", id)); - siteReserveService.clear(Cnd.NEW().and("sqid", "=", fetch.getSqid())); - } + siteBookingService.cancel(id); return null; } @At @RequiresPermissions("activity.site.reserve") public Object isCanRollBack(String id) { - //根据活动id查询第一个审核节点 - ActivitySiteReserve fetch = siteReserveService.fetch(id); - String moduleName = activityCommonService.findModuleNameByActivityId(fetch.getSite_id()); - Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName); - - return fetch.getReserve_state() > stateCode; + ActivitySiteReserve b=siteReserveService.fetch(id); + if(b==null) return true; + return b.getReserve_state()!=siteBookingService.firstState(b.getReserve_type()) || b.getAuditList()!=null && !b.getAuditList().isEmpty(); } @At @ViReturn @RequiresPermissions("activity.site.reserve") + /** data 为场地、类型、协会及事由;days 为日期数组,起止时间取 data;返回新申请 sqid。 */ + @Aop(TransAop.READ_COMMITTED) public Object doAdd(@Param("data") ActivitySiteReserve activitySiteReserve, @Param("days") String[] days) { - - if(activitySiteReserve.getReserve_type() == 1 && days.length > 1) { - return Result.error("个人预约只支持预约明天的时间"); - } - - //主要来查询个人预约时的人数上限 - ActivitySiteInfo siteInfo = siteInfoService.dao().fetch(ActivitySiteInfo.class, activitySiteReserve.getSite_id()); - String moduleName = activityCommonService.findModuleNameByActivityId(activitySiteReserve.getSite_id()); - Integer limitNum = siteInfo.getLimitNum(); - - for (String day : days) { - String time = day + " " + activitySiteReserve.getStart_time(); - int compare = DateUtil.compare(new Date(), DateUtil.parse(time), "yyyy-MM-dd HH:mm"); - if(compare > 0) { - return Result.error("您预约的【%s】时间已过".formatted(time)); - } - if(activitySiteReserve.getReserve_type() == 1 && !day.equals(DateUtil.format(DateUtil.offsetDay(new Date(), 1), "yyyy-MM-dd"))) { - return Result.error("个人预约只支持预约明天的时间"); - } - - List list = siteReserveService.query(Cnd.NEW() - .and("reserve_day", "=", day) - .and("start_time", "=", activitySiteReserve.getStart_time()) - .and("end_time", "=", activitySiteReserve.getEnd_time()) - .and("site_id", "=", activitySiteReserve.getSite_id()) - .and(new Static(" reserve_state in (select stateId from audit_state where stateAuditType != 1 and module = '%s')".formatted(moduleName)))); - ActivitySiteReserve reserve = list.stream().filter(o -> o.getReserve_person_id().equals(ShiroUtil.getPrincipalProperty("id"))).findAny().orElse(null); - if (reserve != null) { - return Result.error("您已预约该时间段!"); - } - if(activitySiteReserve.getReserve_type() == 1) { - if((list.size() + 1) > limitNum) { - return Result.error("【%s】时间段预约人数已满!".formatted(time)); - } - } else { - if(list.size() > 0) { - return Result.error("【%s】时间段已有预约!".formatted(time)); - } - } - } - - String r = R.UU32(); - activitySiteReserve.setSqid(r); - - Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName); - - if (stateCode == null) { - return null; - } - - for (String d : days) { - activitySiteReserve.setReserve_day(d); - activitySiteReserve.setReserve_state(stateCode); - activitySiteReserve.setReserve_person_id(ShiroUtil.getPrincipalProperty("id").toString()); - siteReserveService.insert(activitySiteReserve); - } - - //发送微信提醒审核人 - //查询状态码对应的审核人 - Sql sql = Sqls.create(""" - select userId,u.loginname,u.username,u.unitname from audit_state_user asu - left join `user` u on u.id = asu.userId left join audit_state s on s.stateId=asu.stateId $condition - """); - Cnd cnd = Cnd.NEW(); - cnd.and("asu.stateId", "=", stateCode); - cnd.and("s.stateAuditType", "=", "0"); - if ("infantRoom".equals(moduleName)) { - //获取当前登录人的单位id - String unitId = ShiroUtil.getPrincipalProperty("unitid").toString(); - cnd.and("u.unitid", "=", unitId); - } - sql.setCondition(cnd); - List list = siteReserveService.list(sql); - - list.forEach(item -> { - String content = "%s老师您好!%s老师预约%s日使用%s,请您通过门户网站或点击详情登录智慧工会平台进行审核。" - .formatted(item.getString("username"), - activitySiteReserve.getReserve_person(), - StringUtils.join(days, ","), - siteInfoService.fetch(activitySiteReserve.getSite_id()).getName()); - - Sys_user sys_user = sysUserService.fetch(item.getString("userId")); - NutMap map = new NutMap(); - map.setv("keyword1", NutMap.NEW().setv("value", "场地预约")); - map.setv("keyword2", NutMap.NEW().setv("value", item.getString("username") - + "(" + item.getString("loginname") + ")")); - map.setv("keyword3", NutMap.NEW().setv("value", item.getString("unitname"))); - map.setv("keyword4", NutMap.NEW().setv("value", content)); - map.setv("keyword5", NutMap.NEW().setv("value", DateUtil.now())); - //msgApi.sendMsg(sys_user.getWeAppOpenid(), msgApi.DSH_TEMPLATE_ID, Globals.AppDomain + "/mobile/activity/site/audit", map); - }); - return null; + List slots = new ArrayList<>(); + if (activitySiteReserve == null || days == null) throw new IllegalArgumentException("请选择预约日期"); + for (String day : days) slots.add(NutMap.NEW().setv("day",day).setv("start_time",activitySiteReserve.getStart_time()).setv("end_time",activitySiteReserve.getEnd_time())); + return siteBookingService.submit(activitySiteReserve,slots); } @At @@ -267,7 +179,7 @@ public class SiteReserveController { @Param(value = "siteType",required = false) String siteType) { String sb = "SELECT " + "asi.*," + - "( SELECT count( 1 ) FROM activity_site_reserve WHERE site_id = asi.id AND reserve_state = (select stateId from audit_state where module = (select moduleName from activity_type where id = asi.typeId) and stateAuditType = 3) "; + "( SELECT count( 1 ) FROM activity_site_reserve WHERE site_id = asi.id AND reserve_state = 4030 "; if (StrUtil.isNotBlank(time)) { sb += " AND left(reserve_day, 7) = '" + time + "'"; } @@ -276,6 +188,7 @@ public class SiteReserveController { Sql sql = Sqls.create(sb); Cnd cnd = Cnd.NEW(); cnd.and("state", "=", true); + cnd.and("typeId", "in", Sqls.create("select id from activity_type where enabled=1")); if (StrUtil.isNotBlank(siteType)) { cnd.and("typeId", "=", siteType); } @@ -302,28 +215,14 @@ public class SiteReserveController { @RequiresPermissions("activity.site.reserve") public Object findReserveInfo(@Param(value = "siteId",required = false) String siteId, @Param(value = "day",required = false) String day) { - Sql sql = Sqls.create("SELECT ar.*,`as`.stateAuditType FROM activity_site_reserve ar left join audit_state `as` on ar.reserve_state=`as`.stateId $condition"); - - String moduleName = activityCommonService.findModuleNameByActivityId(siteId); - //根据moduleName找审核失败的数据 - Sql s = Sqls.create(""" - select - stateId - from - audit_state - where - stateAuditType = 1 and - module = @module - """).setParam("module", moduleName); - - List list = siteReserveService.list(s); - List stateList = list.stream().map(o -> o.getString("stateId")).collect(Collectors.toList()); + Sql sql = Sqls.create("SELECT ar.*,`as`.stateAuditType FROM activity_site_reserve ar left join (select state_id stateId,state_name stateName,state_color stateColor,'activity_site' module,case when state_id=4030 then 3 when state_id in (4040,4050) then 1 else 0 end stateAuditType from state where belong='activity_site') `as` on ar.reserve_state=`as`.stateId $condition"); Cnd cnd = Cnd.NEW(); cnd.and("site_id", "=", siteId); + cnd.and("reserve_state", "not in", java.util.Arrays.asList(4040,4050)); if (StrUtil.isNotBlank(day)) { cnd.and("reserve_day", "=", day); - cnd.and("reserve_state", "not in ", stateList); + } sql.setCondition(cnd); return siteReserveService.list(sql); @@ -335,7 +234,7 @@ public class SiteReserveController { public Object checkReserve(String reserve_day, String start_time, String end_time, String site_id) { Cnd cnd = Cnd.NEW(); cnd.and("site_id", "=", site_id) - .and("reserve_state", "!=", 40) + .and("reserve_state", "not in", java.util.Arrays.asList(4040,4050)) .and("reserve_day", "=", reserve_day); SqlExpressionGroup group = new SqlExpressionGroup(); group.or("start_time", "<=", start_time).and("end_time", ">=", start_time); @@ -348,84 +247,10 @@ public class SiteReserveController { @ViReturn @RequiresPermissions("activity.site.reserve") public Object findOne(String id) { - ActivitySiteReserve fetch = siteReserveService.fetch(id); - Sql sql = Sqls.create(""" - SELECT - asr.*, - asi.`name` site_name, - ass.`stateName` state_name, - ass.stateAuditType, - su.username, - sm.username smusername, - su.sex, - su.loginname, - count(sqid) days, - GROUP_CONCAT(reserve_day order by reserve_day asc) concat_day - FROM - activity_site_reserve asr - LEFT JOIN activity_site_info asi ON asi.id = asr.site_id - LEFT JOIN audit_state ass ON ass.stateId = asr.reserve_state - LEFT JOIN sys_user su ON su.id = asr.reserve_person_id - LEFT JOIN sys_user sm ON sm.id = asr.site_manager_id - WHERE - asr.sqid = @id - group by sqid - """).setParam("id", fetch.getSqid()); - NutMap record = (NutMap) Daos.query(siteReserveService.dao(), sql.toString(), Sqls.callback.map()); - String auditList = record.getString("auditList"); - if (auditList != null) { - List nutMaps = Json.fromJsonAsList(NutMap.class, auditList); - nutMaps.forEach(item -> { - item.put("auditListName", item.getBoolean("auditState") ? "审核通过" : "审核拒绝"); - Sys_user auditUSer = sysUserService.fetch(item.getString("auditUser")); - item.put("auditUserName", auditUSer.getUsername()); - item.put("auditUserUnionName", sysUnitService.fetch(auditUSer.getUnitid()).getName()); - Sql sqlStr = Sqls.create("select stateName from audit_state where stateId = '" + item.getString("stateCode") + "'"); - Record re = sysUserService.list(sqlStr).get(0); - item.put("auditStateName", re.getString("stateName")); - }); - record.put("auditListTable", nutMaps); - } else { - record.put("auditListTable", new ArrayList<>()); - } - - Cnd cnd = Cnd.NEW(); - String site_id = record.getString("site_id"); - String reserve_day = record.getString("reserve_day"); - - String moduleName = activityCommonService.findModuleNameByActivityId(record.getString("site_id")); - //根据moduleName找审核失败的数据 - Sql s = Sqls.create(""" - select - stateId - from - audit_state - where - stateAuditType = 1 and - module = @module - """).setParam("module", moduleName); - - List list = siteReserveService.listMap(s); - List stateList = list.stream().map(o -> o.getString("stateId")).collect(Collectors.toList()); - - Sql i = Sqls.create(""" - SELECT - ar.*, - `as`.stateAuditType - FROM - activity_site_reserve ar - left join audit_state `as` on `as`.stateId = ar.reserve_state - $condition - """); - cnd.and("site_id", "=", site_id) - //.and("reserve_day", "=", reserve_day) - .and("reserve_state", "not in", stateList); - - i.setCondition(cnd); - List list1 = siteReserveService.listMap(i); - record.put("site_info", list1); - - return record; + return siteBookingService.detail(id); } + /** 返回当前用户有效协会数组(id/name),供个人所属协会展示和协会预约选择。 */ + @At @ViReturn @org.apache.shiro.authz.annotation.RequiresAuthentication + public Object myClubs() { return siteBookingService.myClubs(); } } diff --git a/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteReviewController.java b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteReviewController.java index 709f831..2f1029c 100644 --- a/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteReviewController.java +++ b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteReviewController.java @@ -1,258 +1,10 @@ package io.v.nutz.zhgh.activity.controller.site; - - -import cn.hutool.core.date.DateUtil; -import io.v.nutz.zhgh.activity.models.ActivitySiteInfo; -import io.v.nutz.zhgh.activity.models.ActivitySiteReserve; -import io.v.nutz.zhgh.activity.services.ActivityCommonService; -import io.v.nutz.zhgh.activity.services.SiteInfoService; -import io.v.nutz.zhgh.activity.services.SiteReserveService; -import io.v.nutz.base.annontation.ViReturn; -import io.v.nutz.base.utils.MsgApi; -import io.v.nutz.base.utils.PageUtil; - -import io.v.nutz.base.model.Audit; -import io.v.nutz.base.model.AuditState; -import io.v.nutz.zhgh.msgNotify.service.MsgNotifyService; -import io.v.nutz.sys.models.Sys_user; -import io.v.nutz.sys.services.SysUserService; -import io.v.nutz.web.commons.utils.ShiroUtil; -import org.apache.shiro.authz.annotation.RequiresPermissions; -import org.nutz.dao.Cnd; -import org.nutz.dao.Sqls; -import org.nutz.dao.entity.Record; -import org.nutz.dao.sql.Sql; -import org.nutz.dao.util.cri.SqlExpressionGroup; -import org.nutz.dao.util.cri.Static; -import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; -import org.nutz.lang.Lang; -import org.nutz.lang.Strings; -import org.nutz.lang.util.NutMap; -import org.nutz.mvc.annotation.At; -import org.nutz.mvc.annotation.Ok; -import org.nutz.mvc.annotation.Param; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** - * @author: Aaron - * @create: 2020-12-15 13:52 - * @description: 预约审核 - **/ -@IocBean -@Ok("json:full") -@At("/platform/activity/site/review") +import org.nutz.mvc.annotation.*; +import org.apache.shiro.authz.annotation.RequiresAuthentication; +/** 旧审核入口仅展示三个新入口,不再提供旧审核提交接口。 */ +@IocBean @At("/platform/activity/site/review") @RequiresAuthentication public class SiteReviewController { - - @Inject - private SiteInfoService siteInfoService; - - @Inject - private SiteReserveService siteReserveService; - - @Inject - private ActivityCommonService activityCommonService; - - - - @Inject - private MsgApi msgApi; - - @Inject - private MsgNotifyService msgNotifyService; - - @Inject - private SysUserService sysUserService; - - @At("") - @Ok("beetl:platform/activity/site/SiteReview.html") - @RequiresPermissions("activity.site.review") - public void index() { - } - - @At - @ViReturn - @RequiresPermissions("activity.site.review") - public Object pageData(@Param(value = "month",required = false) String month, - @Param(value = "siteType",required = false) String siteType, - @Param(value = "searchName",required = false) String searchName, - @Param(value = "searchKeyword",required = false) String searchKeyword, - @Param("pageNumber") int pageNumber,@Param("pageSize") int pageSize, - @Param(value = "pageOrderName",required = false) String pageOrderName, - @Param(value = "pageOrderBy",required = false) String pageOrderBy, - @Param(value = "isAudit",required = false) Boolean isAudit) { - Cnd cnd = Cnd.NEW(); - - Sql sql = Sqls.create(""" - SELECT - asr.*, - asi.`name` site_name, - ass.`stateName` state_name, - ass.`stateColor` state_color, - ass.stateAuditType, - count(sqid) days, - GROUP_CONCAT(reserve_day order by reserve_day asc) concat_day - FROM - activity_site_reserve asr - LEFT JOIN - activity_site_info asi ON asi.id = asr.site_id - LEFT JOIN - audit_state ass ON ass.stateId = asr.reserve_state - LEFT JOIN - `user` u on u.id = asr.reserve_person_id - $condition - """); - - /*if (ShiroUtil.hasAnyRoles(new String[]{"gh10"})) { - Sys_user user = (Sys_user) ShiroUtil.getPrincipal(); - cnd.and("u.unitid", "=", user.getUnitid()); - }*/ - - /*if (!ShiroUtil.hasAnyRoles(new String[]{"sysadmin", "A06"})) { - cnd.and(new Static("if(asi.typeId=1, u.unionId = '" + Vi.getUnionId() + "', 1=1)")); - }*/ - - //查询未审核,获取当前用户可以审核的节点 - Sql s = Sqls.create(""" - select stateId from audit_state_user where userId=@userId - """).setParam("userId", ShiroUtil.getPrincipalProperty("id")); - List sList = siteReserveService.list(s); - List stateList = sList.stream().map(o -> o.getString("stateId")).collect(Collectors.toList()); - - List audit = siteInfoService.dao().query(AuditState.class, Cnd.where("stateId", "in", stateList)); - SqlExpressionGroup sqlExpressionGroup = new SqlExpressionGroup(); - audit.forEach(v->{ - if (Strings.isNotBlank(v.getAuditAfterType())){ -// CustomAuditTypeHandle instance = Enum.instance(CustomAuditTypeHandle.class, v.getAuditAfterType()); -// if(instance != null) { -// instance.next(sqlExpressionGroup); -// } - } - }); - if (!sqlExpressionGroup.isEmpty()) { - cnd.and(sqlExpressionGroup); - } - - if (isAudit == null) { - SqlExpressionGroup group = new SqlExpressionGroup(); - group.and(new Static("JSON_CONTAINS(auditList, JSON_OBJECT('auditUser', '" + ShiroUtil.getPrincipalProperty("id") + "'))")) - .or("reserve_state", "in", stateList); - cnd.and(group); - } else if (isAudit) { - //查询已审核 - cnd.and(new Static("JSON_CONTAINS(auditList, JSON_OBJECT('auditUser', '" + ShiroUtil.getPrincipalProperty("id") + "'))")); - } else { - cnd.and("reserve_state", "in", stateList); - } - - if (Strings.isNotBlank(siteType)) { - cnd.and("asi.typeId", "=", siteType); - } - - if (Strings.isNotBlank(month)) { - cnd.and("left(reserve_day, 7)", "=", month); - } - - if (Strings.isNotBlank(searchName) && Strings.isNotBlank(searchKeyword)) { - cnd.where().andLike(searchName, searchKeyword); - } - - if (Strings.isNotBlank(pageOrderName) && Strings.isNotBlank(pageOrderName)) { - cnd.orderBy(pageOrderName, PageUtil.getOrder(pageOrderBy)); - } else { - cnd.asc("asr.reserve_state").desc("u.unitid"); - } - - cnd.groupBy("asi.id"); - - sql.setCondition(cnd); - return siteReserveService.listPage(pageNumber, pageSize, sql); - } - - @At - @ViReturn - @RequiresPermissions("activity.site.review") - public Object doReview(String[] id, Audit audit, Boolean isPass) { - siteReserveService.insert(audit); - - for (String s : id) { - ActivitySiteReserve fetch = siteReserveService.fetch(s); - - Integer stateCode = fetch.getReserve_state(); - Integer afterStateCode = activityCommonService.findAfterStateCode(stateCode, isPass); - - List reserves = siteReserveService.query(Cnd.NEW().and("sqid", "=", fetch.getSqid())); - String days = ""; - for (ActivitySiteReserve siteReserve : reserves) { - - siteReserve.setReserve_state(afterStateCode); - - List auditList = siteReserve.getAuditList(); - if (Lang.isEmpty(auditList)) { - auditList = new ArrayList<>(); - } - - auditList.add(NutMap.NEW().addv("stateCode", stateCode).addv("auditId", audit.getId()) - .addv("auditUser", ShiroUtil.getPrincipalProperty("id")) - .addv("auditState", isPass) - .addv("auditOption", audit.getAuditOpinion()) - ); - siteReserve.setAuditList(auditList); - - days += siteReserve.getReserve_day() + ","; - siteReserveService.update(siteReserve); - } - days = days.substring(0, days.length() - 1); - - Integer successCode = activityCommonService.findSuccessStateCode(fetch.getSite_id()); - ActivitySiteInfo siteInfo = siteInfoService.fetch(fetch.getSite_id()); - - if (afterStateCode.equals(successCode) && siteInfo.getTypeId() == 1) { - String content = "%s老师您好!您提交的%s使用申请已通过审批,如有疑问,欢迎咨询校工会。" - .formatted(fetch.getReserve_person(), siteInfo.getName()); - Sys_user user = sysUserService.dao().fetch(Sys_user.class, fetch.getReserve_person_id()); -// msgApi.sendMsg(content, List.of(user.getLoginname())); - } else if (!afterStateCode.equals(successCode)) { - //发送微信提醒审核人 - //查询状态码对应的审核人 - Sql sql = Sqls.create(""" - select userId,u.loginname,u.username,u.unitname from audit_state_user asu - left join `user` u on u.id = asu.userId left join audit_state s on s.stateId=asu.stateId $condition - """); - Cnd cnd = Cnd.NEW(); - cnd.and("asu.stateId", "=", afterStateCode); - cnd.and("s.stateAuditType", "=", "0"); - String moduleName = activityCommonService.findModuleNameByActivityId(fetch.getSite_id()); - if ("infantRoom".equals(moduleName)) { - //获取当前登录人的单位id - String unitId = ShiroUtil.getPrincipalProperty("unitid").toString(); - cnd.and("u.unitid", "=", unitId); - } - sql.setCondition(cnd); - List list = siteReserveService.list(sql); - - for (Record item : list) { - String content = "%s老师您好!%s老师预约%s日使用%s,请您通过门户网站或点击详情登录智慧工会平台进行审核。" - .formatted(item.getString("username"), - fetch.getReserve_person(), - days, - siteInfo.getName()); - Sys_user sys_user = sysUserService.fetch(item.getString("userId")); - NutMap map = new NutMap(); - map.setv("keyword1", NutMap.NEW().setv("value", "场地预约")); - map.setv("keyword2", NutMap.NEW().setv("value", item.getString("username") - + "(" + item.getString("loginname") + ")")); - map.setv("keyword3", NutMap.NEW().setv("value", item.getString("unitname"))); - map.setv("keyword4", NutMap.NEW().setv("value", content)); - map.setv("keyword5", NutMap.NEW().setv("value", DateUtil.now())); -// msgApi.sendMsg(sys_user.getWeAppOpenid(), msgApi.DSH_TEMPLATE_ID, Globals.AppDomain + "/mobile/activity/site/audit", map); - - } - } - } - return null; - } + @At("") @Ok("beetl:/platform/activity/site/ReviewEntries.html") + public void index() { } } diff --git a/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteSchoolReviewController.java b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteSchoolReviewController.java new file mode 100644 index 0000000..72449d8 --- /dev/null +++ b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteSchoolReviewController.java @@ -0,0 +1,42 @@ +package io.v.nutz.zhgh.activity.controller.site; +import io.v.nutz.base.annontation.ViReturn; +import io.v.nutz.zhgh.activity.services.impl.SiteBookingService; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.nutz.ioc.loader.annotation.*; +import org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.mvc.annotation.*; +import javax.servlet.http.HttpServletRequest; + +/** 校工会审核:PC/H5 共享业务服务,入口固定节点,不能由请求绕过节点限制。 */ +@IocBean @Ok("json:full") @At("/platform/activity/site/review/school") +@RequiresPermissions("activity.site.review.school") +public class SiteSchoolReviewController { + @Inject private SiteBookingService siteBookingService; + @At("") @Ok("beetl:/platform/activity/site/SiteReview.html") + public void index(HttpServletRequest request) { + request.setAttribute("reviewTitle","校工会审核"); + request.setAttribute("reviewApi","/platform/activity/site/review/school"); + } + /** isAudit 为已审核/未审核;searchName 为场地名称或预约人字段,month 为 yyyy-MM,siteType 为类型 ID,返回 list/totalCount。 */ + @At @ViReturn + public Object pageData(Boolean isAudit,String searchName,String searchKeyword,String month,String siteType,int pageNumber,int pageSize) { + return siteBookingService.reviewPage("school",Boolean.TRUE.equals(isAudit),searchName,searchKeyword,month,siteType,pageNumber,pageSize); + } + /** id 是预约记录主键,返回申请、时段及审核历史,服务层校验查看范围。 */ + @At @ViReturn + public Object detail(String id) { return siteBookingService.detail(id); } + /** ids 为预约记录 ID 数组,isPass 为通过/拒绝,auditOpinion 为必填意见;返回 Object,由 @ViReturn 包装 code(0 成功)和 msg(结果说明)。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doReview(String[] ids,Boolean isPass,String auditOpinion) { + siteBookingService.review("school",ids,isPass,auditOpinion); + // Object 返回类型保留 @ViReturn 的统一结果,避免前端收到空响应。 + return null; + } + /** id 为预约记录主键,撤回本人最近一次审核;返回统一 code/msg,成功后恢复当前节点待审。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doRevoke(String id) { + siteBookingService.revokeReview("school",id); + return null; + } +} diff --git a/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteUnionReviewController.java b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteUnionReviewController.java new file mode 100644 index 0000000..22d8aa5 --- /dev/null +++ b/src/main/java/io/v/nutz/zhgh/activity/controller/site/SiteUnionReviewController.java @@ -0,0 +1,42 @@ +package io.v.nutz.zhgh.activity.controller.site; +import io.v.nutz.base.annontation.ViReturn; +import io.v.nutz.zhgh.activity.services.impl.SiteBookingService; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.nutz.ioc.loader.annotation.*; +import org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.mvc.annotation.*; +import javax.servlet.http.HttpServletRequest; + +/** 分工会审核:PC/H5 共享业务服务,入口固定节点,不能由请求绕过节点限制。 */ +@IocBean @Ok("json:full") @At("/platform/activity/site/review/union") +@RequiresPermissions("activity.site.review.union") +public class SiteUnionReviewController { + @Inject private SiteBookingService siteBookingService; + @At("") @Ok("beetl:/platform/activity/site/SiteReview.html") + public void index(HttpServletRequest request) { + request.setAttribute("reviewTitle","分工会审核"); + request.setAttribute("reviewApi","/platform/activity/site/review/union"); + } + /** isAudit 为已审核/未审核;searchName 为场地名称或预约人字段,month 为 yyyy-MM,siteType 为类型 ID,返回 list/totalCount。 */ + @At @ViReturn + public Object pageData(Boolean isAudit,String searchName,String searchKeyword,String month,String siteType,int pageNumber,int pageSize) { + return siteBookingService.reviewPage("union",Boolean.TRUE.equals(isAudit),searchName,searchKeyword,month,siteType,pageNumber,pageSize); + } + /** id 是预约记录主键,返回申请、时段及审核历史,服务层校验查看范围。 */ + @At @ViReturn + public Object detail(String id) { return siteBookingService.detail(id); } + /** ids 为预约记录 ID 数组,isPass 为通过/拒绝,auditOpinion 为必填意见;返回 Object,由 @ViReturn 包装 code(0 成功)和 msg(结果说明)。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doReview(String[] ids,Boolean isPass,String auditOpinion) { + siteBookingService.review("union",ids,isPass,auditOpinion); + // Object 返回类型保留 @ViReturn 的统一结果,避免前端收到空响应。 + return null; + } + /** id 为预约记录主键,撤回本人最近一次审核;返回统一 code/msg,成功后恢复当前节点待审。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doRevoke(String id) { + siteBookingService.revokeReview("union",id); + return null; + } +} diff --git a/src/main/java/io/v/nutz/zhgh/activity/models/ActivitySiteReserve.java b/src/main/java/io/v/nutz/zhgh/activity/models/ActivitySiteReserve.java index 256c797..5d48b13 100644 --- a/src/main/java/io/v/nutz/zhgh/activity/models/ActivitySiteReserve.java +++ b/src/main/java/io/v/nutz/zhgh/activity/models/ActivitySiteReserve.java @@ -19,6 +19,16 @@ import java.util.List; @Comment("活动场地预约信息") public class ActivitySiteReserve extends BaseModel { + @Column + @Comment("协会预约所属协会,提交时校验申请人的有效成员关系") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String clubId; + + @Column + @Comment("提交时所属分工会快照,用于分工会主席审核范围") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String reserve_person_unionid; + @Column @Name @Comment("ID") @@ -127,7 +137,7 @@ public class ActivitySiteReserve extends BaseModel { private String joinUser; @Column - @Comment("预约类型(1.个人预约,2.单位预约)") + @Comment("预约类型(1.个人预约,2.单位预约,3.协会预约)") @ColDefine(type = ColType.INT, width = 10) private Integer reserve_type; } diff --git a/src/main/java/io/v/nutz/zhgh/activity/models/ActivityType.java b/src/main/java/io/v/nutz/zhgh/activity/models/ActivityType.java index ee5008f..4220437 100644 --- a/src/main/java/io/v/nutz/zhgh/activity/models/ActivityType.java +++ b/src/main/java/io/v/nutz/zhgh/activity/models/ActivityType.java @@ -13,6 +13,16 @@ import java.util.List; @Comment("活动场地类型") public class ActivityType { + @Column + @Comment("类型编码,字符串保留前导零") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String code; + + @Column + @Comment("是否启用;停用后不可新增预约") + @Default("1") + private Boolean enabled; + @Id @Comment("ID") @ColDefine(type = ColType.INT) diff --git a/src/main/java/io/v/nutz/zhgh/activity/services/impl/ActivityTypeServiceImpl.java b/src/main/java/io/v/nutz/zhgh/activity/services/impl/ActivityTypeServiceImpl.java index 22f549e..303a517 100644 --- a/src/main/java/io/v/nutz/zhgh/activity/services/impl/ActivityTypeServiceImpl.java +++ b/src/main/java/io/v/nutz/zhgh/activity/services/impl/ActivityTypeServiceImpl.java @@ -1,103 +1,47 @@ package io.v.nutz.zhgh.activity.services.impl; import io.v.nutz.zhgh.activity.models.ActivityType; -import io.v.nutz.zhgh.activity.services.ActivityCommonService; +import io.v.nutz.zhgh.activity.models.ActivitySiteInfo; import io.v.nutz.zhgh.activity.services.ActivityTypeService; -import io.v.nutz.base.model.AuditState; -import io.v.nutz.base.model.AuditStateUser; import io.v.nutz.base.service.impl.ViServiceImpl; -import org.nutz.aop.interceptor.ioc.TransAop; -import org.nutz.dao.Cnd; -import org.nutz.dao.Dao; -import org.nutz.dao.Sqls; -import org.nutz.dao.sql.Sql; -import org.nutz.dao.util.Daos; -import org.nutz.ioc.aop.Aop; -import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.dao.*; import org.nutz.ioc.loader.annotation.IocBean; -import org.nutz.lang.Lang; - -import java.util.List; +import org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.lang.Strings; +/** 类型仅管理分类资料;保留旧模块字段,不再增删审核节点。 */ @IocBean(args = {"refer:dao"}) public class ActivityTypeServiceImpl extends ViServiceImpl implements ActivityTypeService { - - @Inject - private ActivityCommonService activityCommonService; - - public ActivityTypeServiceImpl(Dao dao) { - super(dao); + public ActivityTypeServiceImpl(Dao dao) { super(dao); } + /** 旧接口兼容,不用于新预约流程。 */ + public int findMaxStateId() { return dao().func("audit_state", "max", "stateId"); } + /** 校验编码/名称/启用状态,编码须唯一且保留前导零。 */ + private void validate(ActivityType t) { + if(t == null || Strings.isBlank(t.getCode()) || Strings.isBlank(t.getMeetingTypeName()) || t.getEnabled()==null) + throw new IllegalArgumentException("请填写类型编码、类型名称和是否启用"); + t.setCode(t.getCode().trim()); t.setMeetingTypeName(t.getMeetingTypeName().trim()); + if(t.getCode().length()>50 || t.getMeetingTypeName().length()>50) throw new IllegalArgumentException("编码和名称不能超过50字"); + Cnd c=Cnd.where("code","=",t.getCode()); + if(t.getId()!=null) c.and("id","!=",t.getId()); + if(count(c)>0) throw new IllegalArgumentException("类型编码已存在"); } - - @Override - public int findMaxStateId() { - return dao().func(AuditState.class, "max", "stateId"); - } - - /** - * 根据模块名称查找stateId - * - * @param moduleName - * @return - */ - private String[] findStateIdArray(String moduleName) { - //查询该类型原来关联的stateId - Sql stateSql = Sqls.create("select stateId from audit_state where module = @moduleName").setParam("moduleName", moduleName); - return (String[]) Daos.query(dao(), stateSql.toString(), Sqls.callback.strs()); - } - - /** - * 添加流程信息 - * - * @param activityType - */ - private void insertAuditInfo(ActivityType activityType) { - List auditStateList = activityType.getAuditStateList(); - if (Lang.isNotEmpty(auditStateList)) { - for (int i = 0; i < auditStateList.size(); i++) { - AuditState state = auditStateList.get(i); - state.setModule(activityType.getModuleName()); - state.setMeetingTypeId(activityType.getId()); - dao().insertWith(state, "^auditStateUserList$"); - } - } - } - - @Override + /** 新增类型,不生成审核节点。 */ @Aop(TransAop.READ_COMMITTED) - public void add(ActivityType activityType) { - dao().insert(activityType); - insertAuditInfo(activityType); - } - - @Override + public void add(ActivityType t) { validate(t); t.setSortNum(count()+1); t.setModuleName(t.getCode()); dao().insert(t); } + /** 更新分类资料,保留历史 moduleName 和关联节点。 */ @Aop(TransAop.READ_COMMITTED) - public void edit(ActivityType activityType) { - dao().update(activityType); - - //查询该类型原来关联的stateId - String[] stateIdStr = findStateIdArray(activityType.getModuleName()); - - //删除原来审核状态下的审核人员 - dao().clear(AuditStateUser.class, Cnd.where("stateId", "in", stateIdStr)); - //删除原来的审核状态 - dao().clear(AuditState.class, Cnd.where("stateId", "in", stateIdStr)); - - //添加新的审核流程 - insertAuditInfo(activityType); + public void edit(ActivityType t) { + validate(t); + ActivityType old=fetch(t.getId()); + if(old==null) throw new IllegalArgumentException("类型不存在"); + old.setCode(t.getCode()).setMeetingTypeName(t.getMeetingTypeName()).setEnabled(t.getEnabled()); dao().update(old); } - - @Override + /** 被场地使用的类型不可删除,防止预约历史失去分类。 */ @Aop(TransAop.READ_COMMITTED) - public void delete(ActivityType activityType) { - //查询该类型原来关联的stateId - String[] stateIdStr = findStateIdArray(activityType.getModuleName()); - - //删除原来审核状态下的审核人员 - dao().clear(AuditStateUser.class, Cnd.where("stateId", "in", stateIdStr)); - //删除原来的审核状态 - dao().clear(AuditState.class, Cnd.where("stateId", "in", stateIdStr)); - - dao().delete(activityType); + public void delete(ActivityType t) { + if(t==null) throw new IllegalArgumentException("类型不存在"); + if(dao().count(ActivitySiteInfo.class,Cnd.where("typeId","=",t.getId()))>0) throw new IllegalArgumentException("该类型已被场地使用,请停用而非删除"); + dao().delete(t); } } diff --git a/src/main/java/io/v/nutz/zhgh/activity/services/impl/SiteBookingService.java b/src/main/java/io/v/nutz/zhgh/activity/services/impl/SiteBookingService.java new file mode 100644 index 0000000..a247dab --- /dev/null +++ b/src/main/java/io/v/nutz/zhgh/activity/services/impl/SiteBookingService.java @@ -0,0 +1,402 @@ +package io.v.nutz.zhgh.activity.services.impl; + +import cn.hutool.core.date.DateUtil; +import io.v.nutz.base.model.Audit; +import io.v.nutz.base.service.impl.BaseServiceImpl; +import io.v.nutz.sys.models.Sys_user; +import io.v.nutz.sys.models.Sys_local_process_instance; +import io.v.nutz.sys.models.Sys_local_process_instance_task; +import io.v.nutz.base.page.Pagination; +import io.v.nutz.sys.services.SysLocalProcessService; +import io.v.nutz.web.commons.utils.ShiroUtil; +import io.v.nutz.zhgh.activity.models.*; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.*; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.entity.Record; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.*; +import org.nutz.json.Json; +import org.nutz.lang.Strings; +import org.nutz.lang.util.NutMap; +import java.time.*; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 场地预约固定状态流程。一次 sqid 对应多条时段和一个待办实例; + * 状态名称取自 state,路由由预约类型决定,与场地类型配置无关。 + */ +@IocBean(args = {"refer:dao"}) +public class SiteBookingService extends BaseServiceImpl { + public static final int UNION = 4000, CLUB = 4010, SCHOOL = 4020, PASS = 4030, REJECT = 4040, CANCEL = 4050; + /** 兼容现有日历组件的状态字段;名称统一读取 state,不再读取 audit_state。 */ + public static final String STATES = "(select state_id stateId,state_name stateName,state_color stateColor," + + "case when state_id=4030 then 3 when state_id in (4040,4050) then 1 else 0 end stateAuditType " + + "from state where belong='activity_site')"; + @Inject private SysLocalProcessService sysLocalProcessService; + + public SiteBookingService(Dao dao) { super(dao); } + + /** 返回当前登录人 ID,不接受前端冒用申请人或审核人。 */ + private String uid() { return String.valueOf(ShiroUtil.getPrincipalProperty("id")); } + + /** 返回已通过入会且正常在会的协会,元素为 id、name;不按会长角色限制申请资格。 */ + public List myClubs() { + return listMap(Sqls.create("select distinct c.id,c.name from sys_club_user m join sys_club c on c.id=m.clubid " + + "where m.userid=@uid and m.status=5 and m.isNormal=1 and coalesce(m.delFlag,0)=0 " + + "order by c.name").setParam("uid", uid())); + } + + /** 预约类型 1 个人、2 单位、3 协会,返回应进入的首个状态 ID。 */ + public int firstState(Integer type) { + if (Integer.valueOf(1).equals(type)) return UNION; + if (Integer.valueOf(2).equals(type)) return SCHOOL; + if (Integer.valueOf(3).equals(type)) return CLUB; + throw new IllegalArgumentException("请选择有效的预约类型"); + } + + /** 按节点角色及组织范围查实际审核人;不允许空组织退化为全校范围。 */ + private List reviewers(int node, ActivitySiteReserve booking) { + String role = node == UNION ? "gh01" : node == CLUB ? "club01" : "sysadmin"; + String filter = ""; + if (node == UNION) { + if (Strings.isBlank(booking.getReserve_person_unionid())) throw new IllegalArgumentException("申请人未关联分工会,请先完善所属分工会"); + filter = " and ur.unionid=@org"; + } else if (node == CLUB) { + if (Strings.isBlank(booking.getClubId())) throw new IllegalArgumentException("请选择所属协会"); + filter = " and ur.stid=@org"; + } else if (node != SCHOOL) throw new IllegalArgumentException("当前节点不允许审核"); + return listMap(Sqls.create("select distinct u.id,u.loginname from sys_user_role ur join sys_role r on r.id=ur.roleId " + + "join sys_user u on u.id=ur.userId where r.code=@role and coalesce(u.disabled,0)=0 " + + "and coalesce(u.delFlag,0)=0" + filter).setParam("role", role) + .setParam("org", node == UNION ? booking.getReserve_person_unionid() : booking.getClubId())); + } + + /** 返回非空审核人工号名单,缺少节点负责人时中止事务并提示具体节点。 */ + private List assignments(int node, ActivitySiteReserve booking) { + List names = reviewers(node, booking).stream().map(v -> v.getString("loginname")) + .filter(Strings::isNotBlank).distinct().collect(Collectors.toList()); + if (names.isEmpty()) throw new IllegalArgumentException(node == UNION ? "所属分工会未配置可用的分工会主席" + : node == CLUB ? "所选协会未配置可用的协会会长" : "未配置可用的校工会审核人(系统管理员)"); + return names; + } + + /** stages 是后端固定入口 union/club/school,返回节点 ID,禁止任意传入状态码。 */ + public int node(String stage) { + if ("union".equals(stage)) return UNION; + if ("club".equals(stage)) return CLUB; + if ("school".equals(stage)) return SCHOOL; + throw new IllegalArgumentException("无效的审核入口"); + } + + private String stage(int node) { return node == UNION ? "union" : node == CLUB ? "club" : "school"; } + private String title(int node) { return node == UNION ? "分工会审核" : node == CLUB ? "协会审核" : "校工会审核"; } + private String process(ActivitySiteReserve b) { return "activity_site@" + b.getSqid(); } + + /** + * 保存一次预约。form 提供场地、事由、类型和协会;slots 提供 day/start_time/end_time。 + * 返回新申请 sqid;人员身份、组织、状态在后端生成。锁定场地避免并发超额预约。 + */ + @Aop(TransAop.READ_COMMITTED) + public String submit(ActivitySiteReserve form, List slots) { + if (form == null || Strings.isBlank(form.getSite_id())) throw new IllegalArgumentException("请选择场地"); + int first = firstState(form.getReserve_type()); + if (slots == null || slots.isEmpty()) throw new IllegalArgumentException("请选择预约时段"); + listMap(Sqls.create("select id from activity_site_info where id=@id for update").setParam("id", form.getSite_id())); + ActivitySiteInfo site = dao().fetch(ActivitySiteInfo.class, form.getSite_id()); + if (site == null || !Boolean.TRUE.equals(site.getState())) throw new IllegalArgumentException("场地不存在或已停用"); + ActivityType type = dao().fetch(ActivityType.class, site.getTypeId()); + if (type == null || !Boolean.TRUE.equals(type.getEnabled())) throw new IllegalArgumentException("场地类型已停用"); + if (Strings.isBlank(form.getReserve_cause())) throw new IllegalArgumentException("请填写预约事由"); + Sys_user user = dao().fetch(Sys_user.class, uid()); + form.setReserve_person_id(uid()); + form.setReserve_person(user.getUsername()); + form.setReserve_person_phone(user.getMobile()); + // user 视图按特殊人员工会关系、所属单位计算有效分工会;原表 unionid 可能为空或为历史值。 + // 提交时保存有效组织快照,后续待办和审核范围均使用该快照匹配主席角色。 + List profile=listMap(Sqls.create("select unionid,unitname from `user` where id=@id").setParam("id",uid())); + form.setReserve_person_unionid(profile.isEmpty() ? null : profile.get(0).getString("unionid")); + form.setReserve_person_unit(profile.isEmpty() ? "" : profile.get(0).getString("unitname")); + if (first == CLUB && myClubs().stream().noneMatch(v -> v.getString("id").equals(form.getClubId()))) + throw new IllegalArgumentException("您不是所选协会的有效成员,请重新选择所属协会"); + if (first != CLUB) form.setClubId(null); + // 提交前同时检查首节点和最终节点,避免申请进入流程后无审核人可处理。 + assignments(first, form); + assignments(SCHOOL, form); + List open = Json.fromJsonAsList(NutMap.class, Json.toJson(site.getOpen_hours())); + Set distinct = new HashSet<>(); + for (NutMap slot : slots) { + String day = slot.getString("day"), start = slot.getString("start_time"), end = slot.getString("end_time"); + if (Strings.isBlank(day) || Strings.isBlank(start) || Strings.isBlank(end)) throw new IllegalArgumentException("预约日期和起止时间不能为空"); + LocalDate date = LocalDate.parse(day); + LocalTime startTime = LocalTime.parse(start), endTime = "24:00".equals(end) ? LocalTime.MIDNIGHT : LocalTime.parse(end); + if (!"24:00".equals(end) && !endTime.isAfter(startTime)) throw new IllegalArgumentException("结束时间必须晚于开始时间"); + if (!LocalDateTime.of(date, startTime).isAfter(LocalDateTime.now())) throw new IllegalArgumentException("预约时段已过期"); + if (first == UNION && !date.equals(LocalDate.now().plusDays(1))) throw new IllegalArgumentException("个人预约只支持预约明天的时间"); + if (Boolean.TRUE.equals(site.getWorkday()) && date.getDayOfWeek().getValue() >= 6) throw new IllegalArgumentException("该场地仅支持工作日预约"); + if (open == null || open.stream().noneMatch(v -> start.equals(v.getString("start_time")) && end.equals(v.getString("end_time")))) + throw new IllegalArgumentException("所选时段不在场地开放场次中,请刷新重选"); + if (!distinct.add(day + " " + start + " " + end)) throw new IllegalArgumentException("同一申请不能重复选择时段"); + List existing = query(Cnd.where("site_id", "=", site.getId()).and("reserve_day", "=", day) + .and("start_time", "<", end).and("end_time", ">", start).and("reserve_state", "not in", Arrays.asList(REJECT, CANCEL))); + if (existing.stream().anyMatch(v -> uid().equals(v.getReserve_person_id()))) throw new IllegalArgumentException("您已预约该时间段"); + if (first != UNION && !existing.isEmpty()) throw new IllegalArgumentException("该时段已有预约,协会或单位预约需要空闲时段"); + if (first == UNION && (existing.stream().anyMatch(v -> !Integer.valueOf(1).equals(v.getReserve_type())) + || site.getLimitNum() == null || existing.size() >= site.getLimitNum())) throw new IllegalArgumentException("该时段预约人数已满或已被单位/协会预约"); + } + String sqid = UUID.randomUUID().toString().replace("-", ""); + for (NutMap slot : slots) { + ActivitySiteReserve row = new ActivitySiteReserve(); + row.setSqid(sqid); row.setSite_id(site.getId()); row.setReserve_type(form.getReserve_type()); + row.setClubId(form.getClubId()); row.setReserve_person_unionid(form.getReserve_person_unionid()); + row.setReserve_person_id(uid()); row.setReserve_person(user.getUsername()); + row.setReserve_person_phone(user.getMobile()); row.setReserve_person_unit(form.getReserve_person_unit()); + row.setReserve_cause(form.getReserve_cause()); row.setReserve_state(first); + row.setReserve_day(slot.getString("day")); row.setStart_time(slot.getString("start_time")); row.setEnd_time(slot.getString("end_time")); + dao().insert(row); + } + form.setSqid(sqid); + sysLocalProcessService.startProcess("场地预约:" + site.getName(), process(form), title(first), uid(), + "/platform/activity/site/reserve", "/mobile/activity/site/info/my"); + createTask(first, form); + return sqid; + } + + /** 创建当前节点待办,所有候选人共用一个任务,一人处理即完成该节点。 */ + @Aop(TransAop.READ_COMMITTED) + public void createTask(int node, ActivitySiteReserve booking) { + String url = "/platform/activity/site/review/" + stage(node); + String mobile = "/mobile/activity/site/audit/" + stage(node); + sysLocalProcessService.createTask(process(booking), stage(node), title(node), uid(), assignments(node, booking), + url, url, mobile, mobile); + sysLocalProcessService.updateProcessNodeName(process(booking), title(node)); + } + + /** 返回指定审核入口的数据范围,分工会/协会使用角色关系上的组织字段。 */ + private String scope(int node) { + if (node == SCHOOL) return "exists(select 1 from sys_user_role ur join sys_role r on r.id=ur.roleId where ur.userId=@uid and r.code='sysadmin')"; + return "exists(select 1 from sys_user_role ur join sys_role r on r.id=ur.roleId where ur.userId=@uid and r.code='" + + (node == UNION ? "gh01" : "club01") + "' and " + + (node == UNION ? "ur.unionid=b.reserve_person_unionid" : "ur.stid=b.clubId") + ")"; + } + + /** + * 查询节点待审/已审申请。isAudit=null 为两者合并;返回标准分页 list/totalCount。 + * 以每个 sqid 的代表行展示,避免按场地分组混合不同申请。 + */ + public Object reviewPage(String stage, Boolean isAudit, String keyword, String month, String typeId, int page, int size) { + return reviewPage(stage, isAudit, "", keyword, month, typeId, page, size); + } + + /** + * PC 指定 searchName=asi.name(场地名称)或 asr.reserve_person(预约人);空值兼容手机端组合搜索。 + * keyword 为关键词,month 为 yyyy-MM,typeId 为场地类型主键;返回 list/totalCount 标准分页结果。 + * 查询字段只映射固定白名单,不把请求提供的字段名拼入 SQL。 + */ + public Object reviewPage(String stage, Boolean isAudit, String searchName, String keyword, String month, String typeId, int page, int size) { + String keywordFilter; + if (Strings.isBlank(searchName)) keywordFilter = "s.name like @like or b.reserve_person like @like"; + else if ("asi.name".equals(searchName)) keywordFilter = "s.name like @like"; + else if ("asr.reserve_person".equals(searchName)) keywordFilter = "b.reserve_person like @like"; + else throw new IllegalArgumentException("请选择有效的查询类型"); + int node = node(stage); + String pending = "(b.reserve_state=" + node + " and " + scope(node) + ")"; + String done = "JSON_CONTAINS(coalesce(b.auditList,'[]'),JSON_OBJECT('auditUser',@uid,'stateCode'," + node + "))"; + String selected = isAudit == null ? "(" + pending + " or " + done + ")" : isAudit ? done : pending; + Sql sql = Sqls.create(baseSelect() + " where b.id=(select min(x.id) from activity_site_reserve x where x.sqid=b.sqid) and " + + selected + " and (@keyword='' or " + keywordFilter + ") " + + "and (@month='' or left(b.reserve_day,7)=@month) and (@typeId='' or s.typeId=@typeId) order by b.opAt desc,b.id") + .setParam("uid", uid()).setParam("keyword", Strings.sNull(keyword)).setParam("like", "%" + Strings.sNull(keyword) + "%") + .setParam("month", Strings.sNull(month)).setParam("typeId", Strings.sNull(typeId)); + Pagination result = listPageMap(Math.max(1,page), Math.min(100,Math.max(1,size)), sql); + // 两端共用后端资格判断;真正撤回仍在加锁后复查,不能信任列表中的旧状态。 + for (Map row : result.>getList()) { + ActivitySiteReserve booking = fetch(String.valueOf(row.get("id"))); + String reason = revokeReason(node, booking); + row.put("canRevoke", reason == null); + row.put("revokeReason", reason); + } + return result; + } + + /** 公共展示字段,保留既有组件所需别名,并显示预约类型和所属协会。 */ + private String baseSelect() { + return "select b.*,u.loginname,u.sex,s.name site_name,c.name club_name,un.unionname union_name,st.stateName state_name,st.stateColor state_color,st.stateAuditType," + + "(select count(*) from activity_site_reserve x where x.sqid=b.sqid) days," + + "(select group_concat(concat(x.reserve_day,' ',x.start_time,'-',x.end_time) order by x.reserve_day,x.start_time separator ';') from activity_site_reserve x where x.sqid=b.sqid) concat_day " + + "from activity_site_reserve b left join sys_user u on u.id=b.reserve_person_id left join activity_site_info s on s.id=b.site_id left join sys_club c on c.id=b.clubId " + + "left join sys_union un on un.id=b.reserve_person_unionid " + + "left join " + STATES + " st on st.stateId=b.reserve_state"; + } + + /** 详情仅允许申请人或对应组织审核人查看,已处理者可以继续查看自己的历史。 */ + public NutMap detail(String id) { + ActivitySiteReserve b = fetch(id); + if (b == null) throw new IllegalArgumentException("预约不存在"); + boolean allowed = uid().equals(b.getReserve_person_id()); + for (int node : new java.util.LinkedHashSet<>(Arrays.asList(firstState(b.getReserve_type()), SCHOOL))) { + if (node == UNION && Strings.isBlank(b.getReserve_person_unionid()) || node == CLUB && Strings.isBlank(b.getClubId())) continue; + allowed |= reviewers(node,b).stream().anyMatch(v -> uid().equals(v.getString("id"))); + } + List history = b.getAuditList() == null ? new ArrayList<>() : b.getAuditList(); + allowed |= history.stream().anyMatch(v -> uid().equals(v.getString("auditUser"))); + if (!allowed) throw new IllegalArgumentException("无权查看该预约"); + NutMap result = listMap(Sqls.create(baseSelect() + " where b.id=@id").setParam("id",id)).get(0); + result.put("auditListTable",history); + // 日历仅携带本次申请的时段,避免通过详情获取其他申请人的资料。 + result.put("site_info", listMap(Sqls.create("select b.*,st.stateAuditType from activity_site_reserve b left join " + + STATES + " st on st.stateId=b.reserve_state where b.sqid=@sqid").setParam("sqid",b.getSqid()))); + return result; + } + + /** + * 审核 ids 中的申请;pass 为明确的通过/拒绝,opinion 为必填意见。 + * 返回 void;事务内锁定申请并检查节点、角色及组织,重复/越权审核不产生历史或待办。 + */ + @Aop(TransAop.READ_COMMITTED) + public void review(String stage, String[] ids, Boolean pass, String opinion) { + int node = node(stage); + if (ids == null || ids.length == 0 || pass == null) throw new IllegalArgumentException("请选择申请及审核结果"); + if (Strings.isBlank(opinion) || opinion.length() > 500) throw new IllegalArgumentException("请填写不超过500字的审核意见"); + Set handled = new HashSet<>(); + // 固定加锁顺序降低批量审核之间的死锁风险。 + List ordered = Arrays.stream(ids).sorted().collect(Collectors.toList()); + for (String id : ordered) { + ActivitySiteReserve b = fetch(id); + if (b == null) throw new IllegalArgumentException("预约不存在,请刷新列表"); + if (!handled.add(b.getSqid())) continue; + listMap(Sqls.create("select id from activity_site_reserve where sqid=@sqid order by id for update").setParam("sqid",b.getSqid())); + b = fetch(id); + if (!Integer.valueOf(node).equals(b.getReserve_state())) throw new IllegalArgumentException("申请已处理或不属于当前审核节点,请刷新列表"); + if (reviewers(node,b).stream().noneMatch(v -> uid().equals(v.getString("id")))) throw new IllegalArgumentException("无权审核该分工会或协会的申请"); + int next = !pass ? REJECT : node == SCHOOL ? PASS : SCHOOL; + if (next == SCHOOL) assignments(SCHOOL,b); + Audit audit = new Audit(); + audit.setAuditor(uid()); audit.setUsername(String.valueOf(ShiroUtil.getPrincipalProperty("username"))); audit.setLoginname(String.valueOf(ShiroUtil.getPrincipalProperty("loginname"))); + audit.setAuditTime(new Date()); audit.setAuditPass(pass); audit.setAuditType(pass ? 1 : 2); audit.setAuditOpinion(opinion); + dao().insert(audit); + List history = b.getAuditList() == null ? new ArrayList<>() : b.getAuditList(); + history.add(NutMap.NEW().setv("stateCode",node).setv("auditId",audit.getId()).setv("auditUser",uid()) + .setv("auditState",pass).setv("auditOption",opinion).setv("auditUserName",audit.getUsername()) + .setv("auditStateName",title(node)).setv("auditListName",pass ? "审核通过" : "审核拒绝") + .setv("auditTime",DateUtil.now()).setv("auditUserUnionName","")); + for (ActivitySiteReserve row : query(Cnd.where("sqid","=",b.getSqid()))) { + row.setReserve_state(next); row.setAuditList(history); dao().update(row); + } + sysLocalProcessService.completeTask(stage,process(b),uid(),opinion); + if (next == SCHOOL) createTask(SCHOOL,b); + else if (next == REJECT) sysLocalProcessService.refuseProcess(process(b),"审核拒绝,流程结束"); + else sysLocalProcessService.completeProcess(process(b)); + } + } + + /** 返回不可撤回原因;仅最近一次有效审核的本人且仍拥有该组织审核资格可以撤回。 */ + private String revokeReason(int node, ActivitySiteReserve booking) { + if (booking == null) return "预约不存在"; + List history = booking.getAuditList(); + if (history == null || history.isEmpty()) return "申请尚未审核或已撤回"; + NutMap last = history.get(history.size() - 1); + if (last.getInt("stateCode") != node) return "下一级已审核,不能撤回当前节点"; + if (!uid().equals(last.getString("auditUser"))) return "只能撤回本人最近一次审核"; + int expected = last.getBoolean("auditState") ? node == SCHOOL ? PASS : SCHOOL : REJECT; + if (!Integer.valueOf(expected).equals(booking.getReserve_state())) return "申请状态已变化,请刷新列表"; + if (reviewers(node,booking).stream().noneMatch(user -> uid().equals(user.getString("id")))) + return "已无该组织的审核权限,不能撤回"; + for (ActivitySiteReserve slot : query(Cnd.where("sqid","=",booking.getSqid()))) { + if (!LocalDateTime.of(LocalDate.parse(slot.getReserve_day()),LocalTime.parse(slot.getStart_time())).isAfter(LocalDateTime.now())) + return "预约时段已经开始,不能撤回"; + } + return null; + } + + /** + * 撤回审核:stage 为固定入口,id 为预约记录主键;恢复同一 sqid 全部时段及当前节点待办。 + * 仅撤回最近一次本人的有效审核,拒绝后恢复须重新校验占用;返回 void,由 Controller 包装结果。 + */ + @Aop(TransAop.READ_COMMITTED) + public void revokeReview(String stage, String id) { + int node = node(stage); + if (Strings.isBlank(id)) throw new IllegalArgumentException("请选择需要撤回的申请"); + ActivitySiteReserve booking = fetch(id); + if (booking == null) throw new IllegalArgumentException("预约不存在,请刷新列表"); + // 与提交保持先锁场地再锁申请的顺序,避免拒绝后恢复与新预约抢占同一时段。 + listMap(Sqls.create("select id from activity_site_info where id=@id for update").setParam("id",booking.getSite_id())); + listMap(Sqls.create("select id from activity_site_reserve where sqid=@sqid order by id for update").setParam("sqid",booking.getSqid())); + booking = fetch(id); + String reason = revokeReason(node,booking); + if (reason != null) throw new IllegalArgumentException(reason); + ActivitySiteInfo site = dao().fetch(ActivitySiteInfo.class,booking.getSite_id()); + ActivityType type = site == null ? null : dao().fetch(ActivityType.class,site.getTypeId()); + if (site == null || !Boolean.TRUE.equals(site.getState()) || type == null || !Boolean.TRUE.equals(type.getEnabled())) + throw new IllegalArgumentException("场地或场地类型已停用,不能恢复待审核预约"); + assignments(node,booking); + List slots = query(Cnd.where("sqid","=",booking.getSqid())); + if (Integer.valueOf(REJECT).equals(booking.getReserve_state())) { + for (ActivitySiteReserve slot : slots) { + List occupied = query(Cnd.where("site_id","=",booking.getSite_id()) + .and("sqid","!=",booking.getSqid()).and("reserve_day","=",slot.getReserve_day()) + .and("start_time","<",slot.getEnd_time()).and("end_time",">",slot.getStart_time()) + .and("reserve_state","not in",Arrays.asList(REJECT,CANCEL))); + boolean conflict = Integer.valueOf(1).equals(booking.getReserve_type()) + ? occupied.stream().anyMatch(row -> !Integer.valueOf(1).equals(row.getReserve_type()) + || row.getReserve_person_id().equals(slot.getReserve_person_id())) + || site.getLimitNum() == null || occupied.size() >= site.getLimitNum() + : !occupied.isEmpty(); + if (conflict) throw new IllegalArgumentException("原预约时段已被占用或人数已满,不能撤回拒绝结果"); + } + } + String processId = process(booking); + Sys_local_process_instance instance = dao().fetch(Sys_local_process_instance.class, + Cnd.where("processUniqueId","=",processId).and("processDeleteFlag","=",false).and("delFlag","=",false)); + Sys_local_process_instance_task task = dao().fetch(Sys_local_process_instance_task.class, + Cnd.where("processUniqueId","=",processId).and("taskUniqueId","=",stage) + .and("status","=",2).and("taskDeleteFlag","=",false).and("delFlag","=",false).desc("id")); + if (instance == null || task == null || !uid().equals(task.getActualOwnerId())) + throw new IllegalArgumentException("审核待办记录不完整,不能撤回,请联系管理员"); + List history = new ArrayList<>(booking.getAuditList()); + NutMap last = history.remove(history.size()-1); + Audit audit = dao().fetch(Audit.class,last.getString("auditId")); + if (audit == null) throw new IllegalArgumentException("审核记录不存在,不能撤回"); + // 原审核保留在 audit,扩展信息记录撤回人、时间及申请关联,不物理删除审核痕迹。 + cn.hutool.json.JSONObject ext = audit.getExt() == null ? new cn.hutool.json.JSONObject() : audit.getExt(); + ext.set("siteRevoke",NutMap.NEW().setv("sqid",booking.getSqid()).setv("node",node) + .setv("userId",uid()).setv("time",DateUtil.now())); + audit.setExt(ext); + dao().update(audit,"ext"); + for (ActivitySiteReserve slot : slots) { + slot.setReserve_state(node); + slot.setAuditList(history); + dao().update(slot); + } + // 失效本次已办及其后续未办记录,保留前级已办;新建本节点待办,兼容首节点拒绝和终审。 + dao().update(Sys_local_process_instance_task.class,Chain.make("taskDeleteFlag",true).add("delFlag",true), + Cnd.where("processUniqueId","=",processId).and("id",">=",task.getId()).and("taskDeleteFlag","=",false)); + instance.setProcessInstanceStatus(1); + instance.setNodeName(title(node)); + dao().update(instance); + createTask(node,booking); + } + + /** 仅申请人可撤销尚未处理的首节点申请;保留取消状态及流程记录。 */ + @Aop(TransAop.READ_COMMITTED) + public void cancel(String id) { + ActivitySiteReserve b = fetch(id); + if (b == null || !uid().equals(b.getReserve_person_id())) throw new IllegalArgumentException("只能撤销本人的申请"); + listMap(Sqls.create("select id from activity_site_reserve where sqid=@sqid order by id for update").setParam("sqid",b.getSqid())); + b=fetch(id); + if (b.getReserve_state() != firstState(b.getReserve_type()) || b.getAuditList()!=null && !b.getAuditList().isEmpty()) + throw new IllegalArgumentException("申请已处理,不能撤销"); + dao().update(ActivitySiteReserve.class,Chain.make("reserve_state",CANCEL),Cnd.where("sqid","=",b.getSqid())); + sysLocalProcessService.refuseProcess(process(b),"申请人已撤销"); + } + + /** 反馈只更新本人申请,option 为不超过200字的使用反馈。 */ + @Aop(TransAop.READ_COMMITTED) + public void feedback(String id,String option) { + ActivitySiteReserve b=fetch(id); + if(b==null || !uid().equals(b.getReserve_person_id())) throw new IllegalArgumentException("只能反馈本人的预约"); + if(option!=null && option.length()>200) throw new IllegalArgumentException("反馈不能超过200字"); + dao().update(ActivitySiteReserve.class,Chain.make("back_option",option),Cnd.where("sqid","=",b.getSqid())); + } +} diff --git a/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteAuditMobileController.java b/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteAuditMobileController.java index 2418f0b..7a2bd0c 100644 --- a/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteAuditMobileController.java +++ b/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteAuditMobileController.java @@ -1,310 +1,10 @@ package io.v.nutz.zhgh.mobile.activity.site; - -import cn.hutool.core.date.DateUtil; -import cn.hutool.core.util.StrUtil; -import cn.wizzer.framework.page.Pagination; -import io.v.nutz.zhgh.activity.models.ActivitySiteInfo; -import io.v.nutz.zhgh.activity.models.ActivitySiteReserve; -import io.v.nutz.zhgh.activity.services.ActivityCommonService; -import io.v.nutz.zhgh.activity.services.SiteInfoService; -import io.v.nutz.zhgh.activity.services.SiteReserveService; -import io.v.nutz.base.annontation.ViReturn; -import io.v.nutz.base.utils.MsgApi; - -import io.v.nutz.base.utils.Vi; -import io.v.nutz.base.enums.AuditTypeEnum; -import io.v.nutz.base.dao.CndPlus; -import io.v.nutz.base.model.Audit; -import io.v.nutz.base.query.PageForm; -import io.v.nutz.sys.models.Sys_user; -import io.v.nutz.sys.services.SysUserService; -import io.v.nutz.web.commons.utils.ShiroUtil; -import org.apache.commons.lang3.StringUtils; -import org.nutz.dao.Cnd; -import org.nutz.dao.Dao; -import org.nutz.dao.Sqls; -import org.nutz.dao.entity.Record; -import org.nutz.dao.sql.Sql; -import org.nutz.dao.util.cri.SqlExpressionGroup; -import org.nutz.dao.util.cri.Static; -import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; -import org.nutz.json.Json; -import org.nutz.lang.Lang; -import org.nutz.lang.util.NutMap; -import org.nutz.mvc.annotation.At; -import org.nutz.mvc.annotation.Ok; -import org.nutz.mvc.annotation.Param; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * @Author JyuHsin - * @Date 2022/5/13 - * @Description - */ -@IocBean -@Ok("json:full") -@At("/mobile/activity/site/audit") +import org.nutz.mvc.annotation.*; +import org.apache.shiro.authz.annotation.RequiresAuthentication; +/** 旧审核入口仅展示三个新入口,不再提供旧审核提交接口。 */ +@IocBean @At("/mobile/activity/site/audit") @RequiresAuthentication public class SiteAuditMobileController { - - @Inject - private SiteInfoService siteInfoService; - - @Inject - private SiteReserveService siteReserveService; - - @Inject - private SysUserService sysUserService; - - @Inject - private ActivityCommonService activityCommonService; - - @Inject - private Dao dao; - - @Inject - private MsgApi msgApi; - - - - @At("") - @Ok("beetl:mobile/activity/site/audit.html") - public void index() { - } - - @At("/pageData") - @Ok("json:full") - @ViReturn - public Object pageData(PageForm pageForm, @Param(value = "typeId", required = false) String typeId) { - - Sql sql = Sqls.create(""" - SELECT - info.*, - type.meetingTypeName, - type.moduleName, - (select unitname from `user` where username = info.contact_person) as unitname, - (select count(DISTINCT sqid) from activity_site_reserve where site_id=info.id and JSON_CONTAINS(auditList, JSON_OBJECT('auditUser', @userid))) as audit - FROM - activity_site_info info - LEFT JOIN activity_type type ON type.id = info.typeId - LEFT JOIN audit_state state ON state.module = type.moduleName - $condition - """).setParam("userid", ShiroUtil.getPrincipalProperty("id")); - CndPlus cnd = CndPlus.create(); - - cnd.andEX("state.stateId", "in", activityCommonService.findStateIdForMeCanAudit()); - cnd.and("info.state", "=", true); - if (StringUtils.isNotBlank(pageForm.getSearchKeyword())) { - SqlExpressionGroup seg = new SqlExpressionGroup(); - seg.orLike("info.name", pageForm.getSearchKeyword()); - seg.orLike("info.address", pageForm.getSearchKeyword()); - cnd.and(seg); - } - if (StrUtil.isNotBlank(typeId)) { - cnd.andEX("info.typeId", "=", typeId); - } - cnd.groupBy("info.id"); - - sql.setCondition(cnd); - Pagination pagination = siteInfoService.listPage(pageForm.getPageNumber(), pageForm.getPageSize(), sql); - - List list = pagination.getList(); - list.forEach(item -> { - Record map = (Record) item; - List canAuditUserByLoginUser = this.getCanAuditUserByLoginUser(map.getString("id"), map.getString("moduleName")); - map.put("no_audit", canAuditUserByLoginUser.size()); - }); - return pagination; - } - - @At("/getLeaveUser") - @Ok("json:full") - @ViReturn - public Object getLeaveUser(@Param(value = "siteId", required = false) String siteId, - @Param(value = "moduleName", required = false) String moduleName, - @Param(value = "auditType", required = false)String auditType) { - - List list = "canAudit".equals(auditType) ? this.getCanAuditUserByLoginUser(siteId, moduleName) : this.getHasAuditUserByLoginUser(siteId); - - return list; - } - - /*根据当前登录用户获取可以审核的用户*/ - public List getCanAuditUserByLoginUser(String siteId, String moduleName) { - Sql sql = Sqls.create(""" - SELECT - ar.*, - u.username, - u.loginname, - u.sex, - u.unitname, - state.stateName, - count(sqid) days, - GROUP_CONCAT(reserve_day order by reserve_day asc) concat_day - FROM - activity_site_reserve ar - LEFT JOIN activity_site_info asi ON asi.id = ar.site_id - left join `user` u on ar.reserve_person_id=u.id - LEFT JOIN audit_state state on state.stateId = ar.reserve_state - $condition - """); - Cnd cnd = Cnd.NEW(); - cnd.andEX("ar.site_id", "=", siteId); - cnd.and("u.username", "is not", null); - cnd.and("u.username", "!=", ""); - cnd.and("ar.reserve_state", "in", Sqls.createf("SELECT stateId FROM audit_state_user WHERE userid = '%s'", ShiroUtil.getPrincipalProperty("id"))); - cnd.and("state.stateAuditType", "in", Lang.list(AuditTypeEnum.AUDIT.getValue())); - - /*if(!ShiroUtil.hasAnyRoles(new String[]{"xghng","A06","sysadmin"})) { - if (ShiroUtil.hasAnyRoles(new String[]{"gh10"})) { - Sys_user user = (Sys_user) ShiroUtil.getPrincipal(); - cnd.and("u.unitid", "=", user.getUnitid()); - } - }*/ - - if (!io.v.nutz.web.commons.utils.ShiroUtil.hasAnyRoles(new String[]{"sysadmin"})) { - cnd.and(new Static("if(asi.typeId=1, u.unitid = '" + Vi.getUnit().getId() + "', 1=1)")); - } - - cnd.groupBy("sqid"); - - sql.setCondition(cnd); - - return listMap(sql); - } - - /*根据当前登录用户获取已经审核的用户*/ - public List getHasAuditUserByLoginUser(String siteId) { - Sql sql = Sqls.create(""" - select - ar.*, - u.username, - u.loginname, - u.unitname, - u.sex, - state.stateName, - count(sqid) days, - GROUP_CONCAT(reserve_day order by reserve_day asc) concat_day - from - activity_site_reserve ar - left join - `user` u on ar.reserve_person_id=u.id - LEFT JOIN - audit_state state on state.stateId = ar.reserve_state - $condition - """); - Cnd cnd = Cnd.NEW(); - cnd.and("site_id", "=", siteId); - cnd.and(new Static("JSON_CONTAINS(auditList, JSON_OBJECT('auditUser', '" + ShiroUtil.getPrincipalProperty("id") + "'))")); - - cnd.groupBy("sqid"); - - sql.setCondition(cnd); - List list = listMap(sql); - list.forEach(item -> { - List listMap = Json.fromJsonAsList(NutMap.class, item.getString("auditList")); - Boolean auditState = false; - for (NutMap map : listMap) { - if (map.getString("auditUser").equals(ShiroUtil.getPrincipalProperty("id"))) { - auditState = map.get("auditState") == null ? null : map.getBoolean("auditState"); - } - } - item.put("auditState", auditState); - }); - return list; - } - - @At("/audit") - @Ok("json:full") - @ViReturn - public Object audit(@Param(value = "ids", required = false) String[] ids, Audit audit, - @Param(value = "isPass", required = false) boolean isPass) { - dao.insert(audit); - for (String id : ids) { - - ActivitySiteReserve fetch = siteReserveService.fetch(id); - - Integer stateCode = fetch.getReserve_state(); - Integer afterStateCode = activityCommonService.findAfterStateCode(stateCode, isPass); - - List reserves = siteReserveService.query(Cnd.NEW().and("sqid", "=", fetch.getSqid())); - String days = ""; - - for (ActivitySiteReserve siteReserve : reserves) { - siteReserve.setReserve_state(afterStateCode); - - List auditIdList = siteReserve.getAuditList(); - if (Lang.isEmpty(auditIdList)) { - auditIdList = new ArrayList<>(); - } - auditIdList.add(NutMap.NEW().addv("stateCode", stateCode) - .addv("auditId", audit.getId()) - .addv("auditUser", ShiroUtil.getPrincipalProperty("id")) - .addv("auditState", isPass) - .addv("auditOption", audit.getAuditOpinion())); - siteReserve.setAuditList(auditIdList); - - days += siteReserve.getReserve_day() + ","; - dao.update(siteReserve); - } - days = days.substring(0, days.length() - 1); - - Integer successCode = activityCommonService.findSuccessStateCode(fetch.getSite_id()); - ActivitySiteInfo siteInfo = siteInfoService.fetch(fetch.getSite_id()); - if (afterStateCode.equals(successCode) && siteInfo.getTypeId() == 1) { - String content = "%s老师您好!您提交的%s使用申请已通过审批,在预约使用期间可刷校园卡进入母婴室,如有疑问,欢迎咨询校工会。" - .formatted(fetch.getReserve_person(), siteInfo.getName()); - Sys_user user = sysUserService.fetch(fetch.getReserve_person_id()); - - ArrayList list2 = new ArrayList<>(); - list2.add(Map.of("type", "User", "userId", user.getLoginname(), "name", user.getUsername())); -// msgApi.sendMsg(content, list2, "场地预约", "WeChat", MsgApi.sendMode.normal.name()); - } else if (!afterStateCode.equals(successCode)) { - //发送微信提醒审核人 - //查询状态码对应的审核人 - Sql sql = Sqls.create(""" - select userId,u.loginname,u.username,u.unitname from audit_state_user asu - left join `user` u on u.id = asu.userId left join audit_state s on s.stateId=asu.stateId $condition - """); - Cnd cnd = Cnd.NEW(); - cnd.and("asu.stateId", "=", afterStateCode); - cnd.and("s.stateAuditType", "=", "0"); - String moduleName = activityCommonService.findModuleNameByActivityId(fetch.getSite_id()); - if ("infantRoom".equals(moduleName)) { - //获取当前登录人的单位id - String unitId = io.v.nutz.web.commons.utils.ShiroUtil.getPrincipalProperty("unitid").toString(); - cnd.and("u.unitid", "=", unitId); - } - sql.setCondition(cnd); - List list = siteReserveService.list(sql); - - for (Record item : list) { - String content = "%s老师您好!%s老师预约%s日使用%s,请您通过门户网站或点击详情登录智慧工会平台进行审核。" - .formatted(item.getString("username"), - fetch.getReserve_person(), - days, - siteInfo.getName()); - Sys_user sys_user = sysUserService.fetch(item.getString("userId")); - NutMap map = new NutMap(); - map.setv("keyword1", NutMap.NEW().setv("value", "场地预约")); - map.setv("keyword2", NutMap.NEW().setv("value", item.getString("username") - + "(" + item.getString("loginname") + ")")); - map.setv("keyword3", NutMap.NEW().setv("value", item.getString("unitname"))); - map.setv("keyword4", NutMap.NEW().setv("value", content)); - map.setv("keyword5", NutMap.NEW().setv("value", DateUtil.now())); -// msgApi.sendMsg(sys_user.getWeAppOpenid(), msgApi.DSH_TEMPLATE_ID, Globals.AppDomain + "/mobile/activity/site/audit", map); - } - } - } - return null; - } - - public List listMap(Sql sql) { - sql.setCallback(Sqls.callback.maps()); - dao.execute(sql); - return sql.getList(NutMap.class); - } + @At("") @Ok("beetl:/platform/activity/site/ReviewEntries.html") + public void index() { } } diff --git a/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteClubAuditMobileController.java b/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteClubAuditMobileController.java new file mode 100644 index 0000000..5bfa0d4 --- /dev/null +++ b/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteClubAuditMobileController.java @@ -0,0 +1,42 @@ +package io.v.nutz.zhgh.mobile.activity.site; +import io.v.nutz.base.annontation.ViReturn; +import io.v.nutz.zhgh.activity.services.impl.SiteBookingService; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.nutz.ioc.loader.annotation.*; +import org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.mvc.annotation.*; +import javax.servlet.http.HttpServletRequest; + +/** 协会审核:PC/H5 共享业务服务,入口固定节点,不能由请求绕过节点限制。 */ +@IocBean @Ok("json:full") @At("/mobile/activity/site/audit/club") +@RequiresPermissions("activity.site.review.club") +public class SiteClubAuditMobileController { + @Inject private SiteBookingService siteBookingService; + @At("") @Ok("beetl:/mobile/activity/site/audit.html") + public void index(HttpServletRequest request) { + request.setAttribute("reviewTitle","协会审核"); + request.setAttribute("reviewApi","/mobile/activity/site/audit/club"); + } + /** isAudit 为未审/已审/null全部;keyword/month/typeId 为筛选条件,返回分页 list/totalCount。 */ + @At @ViReturn + public Object pageData(Boolean isAudit,String searchKeyword,String month,String typeId,int pageNumber,int pageSize) { + return siteBookingService.reviewPage("club",isAudit,searchKeyword,month,typeId,pageNumber,pageSize); + } + /** id 是预约记录主键,返回申请、时段及审核历史,服务层校验查看范围。 */ + @At @ViReturn + public Object detail(String id) { return siteBookingService.detail(id); } + /** ids 为预约记录 ID 数组,isPass 为通过/拒绝,auditOpinion 为必填意见;返回 Object,由 @ViReturn 包装 code(0 成功)、msg(结果说明)。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doReview(String[] ids,Boolean isPass,String auditOpinion) { + siteBookingService.review("club",ids,isPass,auditOpinion); + // 非 void 签名保留拦截器生成的统一结果,避免审核成功却返回空响应。 + return null; + } + /** id 为预约记录主键,撤回本人最近一次审核;返回统一 code/msg,成功后恢复当前节点待审。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doRevoke(String id) { + siteBookingService.revokeReview("club",id); + return null; + } +} diff --git a/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteInfoMobileController.java b/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteInfoMobileController.java index d2905be..6074475 100644 --- a/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteInfoMobileController.java +++ b/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteInfoMobileController.java @@ -31,6 +31,9 @@ import org.nutz.json.Json; import org.nutz.lang.random.R; import org.nutz.lang.util.NutMap; import org.nutz.mvc.annotation.At; +import io.v.nutz.zhgh.activity.services.impl.SiteBookingService; +import org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; @@ -45,9 +48,12 @@ import java.util.List; */ @IocBean @Ok("json:full") +@org.apache.shiro.authz.annotation.RequiresPermissions("activity.site.reserve") @At("/mobile/activity/site/info") public class SiteInfoMobileController { + @Inject private SiteBookingService siteBookingService; + @Inject private SiteInfoService siteInfoService; @@ -88,20 +94,20 @@ public class SiteInfoMobileController { Sql sql = Sqls.create(""" SELECT - info.*, - type.meetingTypeName, - type.moduleName, - (select unitname from `user` where username = info.contact_person) as unitname, - ( SELECT count( 1 ) FROM activity_site_reserve WHERE site_id = info.id AND reserve_state = (select stateId from audit_state where module = (select moduleName from activity_type where id = info.typeId) and stateAuditType = 3)) count + info.*, + type.meetingTypeName, + type.moduleName, + (select unitname from `user` where username = info.contact_person) as unitname, + ( SELECT count( 1 ) FROM activity_site_reserve WHERE site_id = info.id AND reserve_state = 4030) count FROM - activity_site_info info - LEFT JOIN activity_type type ON type.id = info.typeId - LEFT JOIN audit_state state ON state.module = type.moduleName + activity_site_info info + LEFT JOIN activity_type type ON type.id = info.typeId $condition """); Cnd cnd = Cnd.NEW(); cnd.and("info.state", "=", true); + cnd.and("type.enabled","=",true); if (StringUtils.isNotBlank(pageForm.getSearchKeyword())) { SqlExpressionGroup seg = new SqlExpressionGroup(); seg.orLike("info.name", pageForm.getSearchKeyword()); @@ -147,7 +153,7 @@ public class SiteInfoMobileController { from activity_site_reserve ar left join activity_site_info info on info.id = ar.site_id - left join audit_state `as` on ar.reserve_state=`as`.stateId + left join (select state_id stateId,state_name stateName,state_color stateColor,'activity_site' module,case when state_id=4030 then 3 when state_id in (4040,4050) then 1 else 0 end stateAuditType from state where belong='activity_site') `as` on ar.reserve_state=`as`.stateId $condition """); @@ -161,7 +167,7 @@ public class SiteInfoMobileController { if (StrUtil.isNotBlank(typeId)) { cnd.andEX("info.typeId", "=", typeId); } - if (!timeSwitch && StrUtil.isNotBlank(time)) { + if (!Boolean.TRUE.equals(timeSwitch) && StrUtil.isNotBlank(time)) { cnd.and("left(ar.reserve_day, 7)", "=", time); } cnd.and("reserve_person_id", "=", ShiroUtil.getPrincipalProperty("id")); @@ -183,26 +189,18 @@ public class SiteInfoMobileController { @At("/rollback") @Ok("json:full") @ViReturn + @Aop(TransAop.READ_COMMITTED) public Object rollback(String id) { - //根据活动id查询第一个审核节点 - ActivitySiteReserve fetch = siteReserveService.fetch(id); - String moduleName = activityCommonService.findModuleNameByActivityId(fetch.getSite_id()); - Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName); - - if (fetch.getReserve_state() > stateCode) { - return Result.error("当前状态不能进行撤销操作"); - } - ActivitySiteReserve siteReserve = siteReserveService.fetch(id); - siteReserveService.clear(Cnd.NEW().and("sqid", "=", siteReserve.getSqid())); + siteBookingService.cancel(id); return null; } @At("/backOption") @Ok("json:full") @ViReturn + @Aop(TransAop.READ_COMMITTED) public Object backOption(String id, String option) { - ActivitySiteReserve fetch = siteReserveService.fetch(id); - siteReserveService.update(Chain.make("back_option", option), Cnd.NEW().and("sqid", "=", fetch.getSqid())); + siteBookingService.feedback(id,option); return null; } @@ -213,7 +211,6 @@ public class SiteInfoMobileController { //获取场地的开放时间 ActivitySiteInfo fetch = siteInfoService.fetch(siteId); - String moduleName = activityCommonService.findModuleNameByActivityId(siteId); List hours = fetch.getOpen_hours(); Integer limitNum = fetch.getLimitNum(); @@ -240,10 +237,10 @@ public class SiteInfoMobileController { .and("start_time", "=", startTime) .and("end_time", "=", endTime) .and("site_id", "=", siteId) - .and(new Static(" reserve_state in (select stateId from audit_state where stateAuditType != 1 and module = '%s')".formatted(moduleName)))); + .and("reserve_state", "not in", java.util.Arrays.asList(4040,4050))); //如果这个时间段被单位预约了,直接显示约满 - ActivitySiteReserve reserve1 = list.stream().filter(o -> o.getReserve_type() == 2).findAny().orElse(null); + ActivitySiteReserve reserve1 = list.stream().filter(o -> o.getReserve_type() != 1).findAny().orElse(null); if(reserve1 != null) { item.put("code", -2); item.put("msg", "已约满"); @@ -274,111 +271,11 @@ public class SiteInfoMobileController { @At("/reserveDo") @Ok("json:full") @ViReturn - public Object reserveDo(String siteId, String times, String message, Integer reserve_type) { - String moduleName = activityCommonService.findModuleNameByActivityId(siteId); - Integer stateCode = activityCommonService.findStartStateCodeByModuleName(moduleName); - - if (stateCode == null) { - return null; - } - - Sys_user user = (Sys_user) ShiroUtil.getPrincipal(); - List li = new ArrayList<>(); - - List nutMaps = Json.fromJsonAsList(NutMap.class, times); - - //主要来查询个人预约时的人数上限 - ActivitySiteInfo siteInfo = siteInfoService.dao().fetch(ActivitySiteInfo.class, siteId); - Integer limitNum = siteInfo.getLimitNum(); - - for (NutMap item : nutMaps) { - String time = item.getString("day") + " " + item.getString("start_time"); - int compare = DateUtil.compare(new Date(), DateUtil.parse(time), "yyyy-MM-dd HH:mm"); - if(compare > 0) { - return Result.error("您预约的【%s】时间已过".formatted(time)); - } - if(reserve_type == 1 && !item.getString("day").equals(DateUtil.format(DateUtil.offsetDay(new Date(), 1), "yyyy-MM-dd"))) { - return Result.error("个人预约只支持预约明天的时间"); - } - - List list = siteReserveService.query(Cnd.NEW() - .and("reserve_day", "=", item.getString("day")) - .and("start_time", "=", item.getString("start_time")) - .and("end_time", "=", item.getString("end_time")) - .and("site_id", "=", siteId) - .and(new Static(" reserve_state in (select stateId from audit_state where stateAuditType != 1 and module = '%s')".formatted(moduleName)))); - ActivitySiteReserve reserve = list.stream().filter(o -> o.getReserve_person_id().equals(ShiroUtil.getPrincipalProperty("id"))).findAny().orElse(null); - if (reserve != null) { - return Result.error("您已预约该时间段!"); - } - if(reserve_type == 1) { - if((list.size() + 1) > limitNum) { - return Result.error("【%s】时间段预约人数已满!".formatted(time)); - } - } else { - if(list.size() > 0) { - return Result.error("【%s】时间段已有预约!".formatted(time)); - } - } - } - - String str = ""; - String r = R.UU32(); - for (NutMap item : nutMaps) { - ActivitySiteReserve as = new ActivitySiteReserve(); - as.setSqid(r); - as.setSite_id(siteId); - as.setReserve_person(user.getUsername()); - as.setReserve_person_id(user.getId()); - as.setReserve_person_unit(user.getUnit() == null ? null : user.getUnit().getName()); - as.setReserve_person_phone(user.getMobile()); - as.setReserve_cause(message); - as.setReserve_state(stateCode); - as.setReserve_day(item.getString("day")); - as.setStart_time(item.getString("start_time")); - as.setEnd_time(item.getString("end_time")); - as.setReserve_type(reserve_type); - str += item.getString("day") + ","; - li.add(as); - } - str = str.substring(0, str.length() - 1); - - siteReserveService.insert(li); - - //发送微信提醒审核人 - //查询状态码对应的审核人 - Sql sql = Sqls.create(""" - select userId,u.loginname,u.username,u.unitname from audit_state_user asu - left join `user` u on u.id = asu.userId left join audit_state s on s.stateId=asu.stateId $condition - """); - Cnd cnd = Cnd.NEW(); - cnd.and("asu.stateId", "=", stateCode); - cnd.and("s.stateAuditType", "=", "0"); - if ("infantRoom".equals(moduleName)) { - //获取当前登录人的单位id - String unitId = ShiroUtil.getPrincipalProperty("unitid").toString(); - cnd.and("u.unitid", "=", unitId); - } - sql.setCondition(cnd); - List list = siteReserveService.list(sql); - - for (Record item : list) { - String content = "%s老师您好!%s老师预约%s日使用%s,请您通过门户网站或点击详情登录智慧工会平台进行审核。" - .formatted(item.getString("username"), - user.getUsername(), - str, - siteInfoService.fetch(siteId).getName()); - - Sys_user sys_user = sysUserService.fetch(item.getString("userId")); - NutMap map = new NutMap(); - map.setv("keyword1", NutMap.NEW().setv("value", "场地预约")); - map.setv("keyword2", NutMap.NEW().setv("value", item.getString("username") - + "(" + item.getString("loginname") + ")")); - map.setv("keyword3", NutMap.NEW().setv("value", item.getString("unitname"))); - map.setv("keyword4", NutMap.NEW().setv("value", content)); - map.setv("keyword5", NutMap.NEW().setv("value", DateUtil.now())); -// msgApi.sendMsg(sys_user.getWeAppOpenid(), msgApi.DSH_TEMPLATE_ID, Globals.AppDomain + "/mobile/activity/site/audit", map); - } - return null; + /** siteId 为场地;times 为 day/start_time/end_time 数组 JSON;message 为事由;reserve_type 为1/2/3,clubId 为协会预约所属协会;返回 sqid。 */ + @Aop(TransAop.READ_COMMITTED) + public Object reserveDo(String siteId, String times, String message, Integer reserve_type, String clubId) { + ActivitySiteReserve form=new ActivitySiteReserve(); + form.setSite_id(siteId);form.setReserve_cause(message);form.setReserve_type(reserve_type);form.setClubId(clubId); + return siteBookingService.submit(form,Json.fromJsonAsList(NutMap.class,times)); } } diff --git a/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteSchoolAuditMobileController.java b/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteSchoolAuditMobileController.java new file mode 100644 index 0000000..c792a17 --- /dev/null +++ b/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteSchoolAuditMobileController.java @@ -0,0 +1,42 @@ +package io.v.nutz.zhgh.mobile.activity.site; +import io.v.nutz.base.annontation.ViReturn; +import io.v.nutz.zhgh.activity.services.impl.SiteBookingService; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.nutz.ioc.loader.annotation.*; +import org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.mvc.annotation.*; +import javax.servlet.http.HttpServletRequest; + +/** 校工会审核:PC/H5 共享业务服务,入口固定节点,不能由请求绕过节点限制。 */ +@IocBean @Ok("json:full") @At("/mobile/activity/site/audit/school") +@RequiresPermissions("activity.site.review.school") +public class SiteSchoolAuditMobileController { + @Inject private SiteBookingService siteBookingService; + @At("") @Ok("beetl:/mobile/activity/site/audit.html") + public void index(HttpServletRequest request) { + request.setAttribute("reviewTitle","校工会审核"); + request.setAttribute("reviewApi","/mobile/activity/site/audit/school"); + } + /** isAudit 为未审/已审/null全部;keyword/month/typeId 为筛选条件,返回分页 list/totalCount。 */ + @At @ViReturn + public Object pageData(Boolean isAudit,String searchKeyword,String month,String typeId,int pageNumber,int pageSize) { + return siteBookingService.reviewPage("school",isAudit,searchKeyword,month,typeId,pageNumber,pageSize); + } + /** id 是预约记录主键,返回申请、时段及审核历史,服务层校验查看范围。 */ + @At @ViReturn + public Object detail(String id) { return siteBookingService.detail(id); } + /** ids 为预约记录 ID 数组,isPass 为通过/拒绝,auditOpinion 为必填意见;返回 Object,由 @ViReturn 包装 code(0 成功)、msg(结果说明)。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doReview(String[] ids,Boolean isPass,String auditOpinion) { + siteBookingService.review("school",ids,isPass,auditOpinion); + // 非 void 签名保留拦截器生成的统一结果,避免审核成功却返回空响应。 + return null; + } + /** id 为预约记录主键,撤回本人最近一次审核;返回统一 code/msg,成功后恢复当前节点待审。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doRevoke(String id) { + siteBookingService.revokeReview("school",id); + return null; + } +} diff --git a/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteUnionAuditMobileController.java b/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteUnionAuditMobileController.java new file mode 100644 index 0000000..88fbcc1 --- /dev/null +++ b/src/main/java/io/v/nutz/zhgh/mobile/activity/site/SiteUnionAuditMobileController.java @@ -0,0 +1,42 @@ +package io.v.nutz.zhgh.mobile.activity.site; +import io.v.nutz.base.annontation.ViReturn; +import io.v.nutz.zhgh.activity.services.impl.SiteBookingService; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.nutz.ioc.loader.annotation.*; +import org.nutz.ioc.aop.Aop; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.mvc.annotation.*; +import javax.servlet.http.HttpServletRequest; + +/** 分工会审核:PC/H5 共享业务服务,入口固定节点,不能由请求绕过节点限制。 */ +@IocBean @Ok("json:full") @At("/mobile/activity/site/audit/union") +@RequiresPermissions("activity.site.review.union") +public class SiteUnionAuditMobileController { + @Inject private SiteBookingService siteBookingService; + @At("") @Ok("beetl:/mobile/activity/site/audit.html") + public void index(HttpServletRequest request) { + request.setAttribute("reviewTitle","分工会审核"); + request.setAttribute("reviewApi","/mobile/activity/site/audit/union"); + } + /** isAudit 为未审/已审/null全部;keyword/month/typeId 为筛选条件,返回分页 list/totalCount。 */ + @At @ViReturn + public Object pageData(Boolean isAudit,String searchKeyword,String month,String typeId,int pageNumber,int pageSize) { + return siteBookingService.reviewPage("union",isAudit,searchKeyword,month,typeId,pageNumber,pageSize); + } + /** id 是预约记录主键,返回申请、时段及审核历史,服务层校验查看范围。 */ + @At @ViReturn + public Object detail(String id) { return siteBookingService.detail(id); } + /** ids 为预约记录 ID 数组,isPass 为通过/拒绝,auditOpinion 为必填意见;返回 Object,由 @ViReturn 包装 code(0 成功)、msg(结果说明)。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doReview(String[] ids,Boolean isPass,String auditOpinion) { + siteBookingService.review("union",ids,isPass,auditOpinion); + // 非 void 签名保留拦截器生成的统一结果,避免审核成功却返回空响应。 + return null; + } + /** id 为预约记录主键,撤回本人最近一次审核;返回统一 code/msg,成功后恢复当前节点待审。 */ + @At @ViReturn @Aop(TransAop.READ_COMMITTED) + public Object doRevoke(String id) { + siteBookingService.revokeReview("union",id); + return null; + } +} diff --git a/src/main/resources/static/assets/mobile/js/popupHistory.js b/src/main/resources/static/assets/mobile/js/popupHistory.js new file mode 100644 index 0000000..0b25e84 --- /dev/null +++ b/src/main/resources/static/assets/mobile/js/popupHistory.js @@ -0,0 +1,83 @@ +// 所有接入页面共用一个弹框历史栈;只注册一次 popstate,系统返回按后进先出关闭。 +window.popupHistory = window.popupHistory || (() => { + let stack = []; + let pending = null; + let navigating = false; + const queue = []; + const owners = new Map(); + const finish = () => { + const callback = pending; + pending = null; + if (callback) callback(); + }; + window.addEventListener('popstate', (event) => { + const marker = event.state && event.state.popupHistoryId; + const index = stack.findIndex((entry) => entry.id === marker); + const removed = stack.splice(index + 1); + removed.reverse().forEach((entry) => { + const callback = owners.get(entry.owner); + if (callback) callback(entry.key); + }); + navigating = false; + finish(); + // 无需回退的排队操作也继续消费;发生下一次历史回退时等待对应 popstate。 + while (!navigating && queue.length) queue.shift()(); + }); + return { + register(owner, callback) { owners.set(owner, callback) }, + push(owner, key) { + if (navigating) { queue.push(() => this.push(owner, key)); return; } + if (stack.some((entry) => entry.owner === owner && entry.key === key)) return; + const id = Date.now().toString() + Math.random().toString(16).slice(2); + stack.push({owner, key, id}); + window.history.pushState(Object.assign({}, window.history.state, {popupHistoryId:id}), ''); + }, + close(owner, key) { + if (navigating) { queue.push(() => this.close(owner, key)); return; } + const top = stack[stack.length - 1]; + if (top && top.owner === owner && top.key === key) { navigating = true; window.history.back(); } + }, + clear(owner, callback) { + if (navigating) { queue.push(() => this.clear(owner, callback)); return; } + const count = stack.filter((entry) => entry.owner === owner).length; + if (!count) { if (callback) callback(); return; } + pending = callback || null; + navigating = true; + window.history.go(-count); + }, + unregister(owner) { + owners.delete(owner); + this.clear(owner); + } + }; +})(); + +// 页面声明 historyPopupKeys 即可同步 Popup/Dialog/ActionSheet;无需页面重复监听浏览器事件。 +window.popupHistoryMixin = { + mounted() { + this._popupOwner = 'page-' + this._uid; + this._popupFromHistory = new Set(); + this._popupUnwatch = []; + window.popupHistory.register(this._popupOwner, (key) => { + this._popupFromHistory.add(key); + this.$set(this, key, false); + this.$nextTick(() => this._popupFromHistory.delete(key)); + }); + (this.historyPopupKeys || []).forEach((key) => { + this._popupUnwatch.push(this.$watch(key, (value) => { + if (this._popupFromHistory.has(key)) return; + if (value) window.popupHistory.push(this._popupOwner, key); + else window.popupHistory.close(this._popupOwner, key); + })); + }); + }, + beforeDestroy() { + (this._popupUnwatch || []).forEach((unwatch) => unwatch()); + window.popupHistory.unregister(this._popupOwner); + }, + methods: { + // 成功提交或页面跳转前清空本页弹框历史,等待回退完成再执行回调。 + popupBack() { window.history.back() }, + clearPopupHistory(callback) { window.popupHistory.clear(this._popupOwner, callback) } + } +}; diff --git a/src/main/resources/static/assets/platform/module/mixins/initTableMixins.js b/src/main/resources/static/assets/platform/module/mixins/initTableMixins.js index 99d1f74..0b58d27 100644 --- a/src/main/resources/static/assets/platform/module/mixins/initTableMixins.js +++ b/src/main/resources/static/assets/platform/module/mixins/initTableMixins.js @@ -95,16 +95,18 @@ const initTableMixins = { const address = url ? url : loc() + "/pageData" sublime.showLoadingbar(); this.tableLoading = true - $.post(address, data ? data : this.pageForm, (data) => { - sublime.closeLoadingbar(); - this.tableLoading = false - if (data.code === 0) { - this.tableData = data.data.list; - this.pageForm.totalCount = data.data.totalCount; + // 公共查询统一在 always 收尾;分页及查询入口继续由各页面复用。 + return $.post(address, data ? data : this.pageForm).then((res) => { + if (res.code === 0) { + this.tableData = res.data.list; + this.pageForm.totalCount = res.data.totalCount; } else { - this.$message.error(data.msg); + this.$message.error(res.msg); } - }, "json"); + }).always(() => { + sublime.closeLoadingbar(); + this.tableLoading = false; + }); }, notifySuccess(msg) { this.$notify({ diff --git a/src/main/resources/static/components/activity/site/ReserveInfo.vue b/src/main/resources/static/components/activity/site/ReserveInfo.vue index 9041fa0..50e234b 100644 --- a/src/main/resources/static/components/activity/site/ReserveInfo.vue +++ b/src/main/resources/static/components/activity/site/ReserveInfo.vue @@ -33,6 +33,8 @@ + {{ {1:'个人预约',2:'单位预约',3:'协会预约'}[viewData.reserve_type] }} + {{viewData.club_name || '—'}} {{ viewData.reserve_person }} {{ viewData.site_name }} {{ viewData.sex }} @@ -123,22 +125,18 @@ module.exports = { } return this.panes.includes(name) }, - async getSiteReserveInfo(id) { - const {data} = await $.get(base + "/platform/activity/site/reserve/findOne", {id}) - return data - }, - async openView(id) { - this.loading = true - this.$forceUpdate() - this.viewData = await this.getSiteReserveInfo(id) - if (this.viewData) { - this.activeName = this.role === true ? "1" : "2" - } else { - this.viewData = {} - this.$message.error("获取场地预约信息失败"); - } - this.$forceUpdate() - this.loading = false + // id 为预约记录主键;返回包含申请、时段和审核历史的 data。 + openView(id) { + this.loading = true; + $.post(base + '/platform/activity/site/reserve/findOne', {id}).then((res) => { + if (res.code === 0) { + this.viewData = res.data; + this.activeName = this.role === true ? '1' : '2'; + } else { + this.viewData = {}; + this.$message.error(res.msg); + } + }).always(() => { this.loading = false }); } } } diff --git a/src/main/resources/views/mobile/activity/site/audit.html b/src/main/resources/views/mobile/activity/site/audit.html index 65a43f2..cc23433 100644 --- a/src/main/resources/views/mobile/activity/site/audit.html +++ b/src/main/resources/views/mobile/activity/site/audit.html @@ -1,551 +1,258 @@ - - + - -
- - - - - - - -
- - - - - +
+ + + + + -
- - -
- - -
-
- -
-
-
- | - {{o.name}} -
-
{{o.address}}
-
-
- {{o.meetingtypename}} -
-
- - - -
- -  联系人:{{o.unitname ? (o.unitname + - '') : '' + '' + o.contact_person}} - - - - 联系方式:{{o.contact_phone}} - -
- -
- - -
- -
-
- 已审{{o.audit}} - - 未审{{o.no_audit}} - -
-
+ + +
+
|{{row.reserve_person}}({{row.loginname}})
+
+
场地名称:{{row.site_name}}
+
所属单位:{{row.reserve_person_unit || '—'}}
+
预约类型:{{typeName(row.reserve_type)}}
+
所属协会:{{row.club_name}}
+
预约时段:{{row.concat_day}}
+
+ + - - -
- - - -
{{desc}}
+
+ + + + + + + +
+
${reviewTitle}
+ + + + + +
+ 取消 + 拒绝 + 通过 +
+
- - - - - -
-
- - 取消选中 - - 全选 -
-
- -
-
- {{item.username}} - {{item.sex}} - {{item.loginname}} - {{item.unitname}} -
-
-
预约时间:
-
-
{{getTime(item, d)}}
-
-
-
- 参加人员:{{item.joinUser}} -
-
- 预约事由:{{item.reserve_cause}} -
-
- 审核状态:{{item.stateName}} -
-
- 反馈意见:{{item.back_option}} -
-
- - - - - - - - - - -
- -
-
-
- 驳回 - - 通过 - -
-
- -
- - - -
{{str}}
- - + +
- - - + diff --git a/src/main/resources/views/mobile/activity/site/common/info.js b/src/main/resources/views/mobile/activity/site/common/info.js new file mode 100644 index 0000000..235aaae --- /dev/null +++ b/src/main/resources/views/mobile/activity/site/common/info.js @@ -0,0 +1,67 @@ +const SITE_AUDIT_INFO = { + template: /*language=HTML*/ ` +
+
加载中...
+ + + + + + + + + + + + + + + + + +
+ `, + props: {reviewApi: {type: String, required: true}}, + data() { return {viewData: {}, activeName: 'basic', expanded: true, detailLoading: false, requestId: 0} }, + methods: { + typeName(type) { return {1:'个人预约',2:'单位预约',3:'协会预约'}[type] || '—' }, + toggleExpanded() { this.$set(this, 'expanded', !this.expanded) }, + // id 为预约记录主键;返回预约字段及 auditListTable 历史,loaded 通知父页核验可审核状态。 + onOpen(id) { + const requestId = this.requestId + 1 + this.$set(this, 'requestId', requestId) + this.$set(this, 'viewData', {}) + this.$set(this, 'activeName', 'basic') + this.$set(this, 'expanded', true) + this.$set(this, 'detailLoading', true) + return $.post(this.reviewApi + '/detail', {id}).then((res) => { + // 忽略快速切换申请留下的旧响应,避免审核表单与详情不一致。 + if (requestId !== this.requestId) return + if (res.code === 0) { + this.$set(this, 'viewData', res.data) + this.$emit('loaded', res.data) + } else { this.$toast.fail(res.msg) } + }).fail(() => { + if (requestId === this.requestId) this.$toast.fail('详情加载失败,请返回重试') + }).always(() => { + if (requestId === this.requestId) this.$set(this, 'detailLoading', false) + }) + } + } +} diff --git a/src/main/resources/views/mobile/activity/site/info.html b/src/main/resources/views/mobile/activity/site/info.html index 7df9da2..0f7359f 100644 --- a/src/main/resources/views/mobile/activity/site/info.html +++ b/src/main/resources/views/mobile/activity/site/info.html @@ -158,19 +158,8 @@ layout("/mobile/platform.html"){ top: 10px; } - .pop { - max-height: 70%; - width: 90%; - padding: 0px 10px 10px 10px; - } - .pop h2 { - text-align: center; - } - .pop h4 { - line-height: 26px; - }
@@ -179,7 +168,7 @@ layout("/mobile/platform.html"){ -
@@ -261,45 +251,13 @@ layout("/mobile/platform.html"){ >
- -
爱心母婴室管理规定
-
一、基本原则
-
-

1.使用对象:有哺乳需求的本校在职女教职工。

-

2.开放时间:工作日8:00--17:30。

-

3.使用制度:实行预约登记制。凡有哺乳需求的女教职工提前向校工会提出使用申请,经审核通过并开通权限后即可使用。

-

4.日常管理:由校工会负责。

-
- -
二、管理人员
-
-

1.负责爱心母婴室使用登记管理。

-

2.定期保养设备,确保正常使用。

-

3.保持室内整洁、卫生安全,做好保洁消毒记录。

-

4.定期收集意见和建议,不断改进服务。

-
-
三、使用人员
-
-

1.遵守学校安全管理制度和爱心母婴室管理规定。

-

2.严禁吸烟,安全使用电器,爱护公物,损坏赔偿。

-

3.保持室内卫生清洁,勿大声喧哗,不影响楼内工作秩序。

-

4.妥善保管自带物品,冰存的母乳须贴上姓名标签并及时取走。

-

5.不得擅自携他人入内,不做和哺乳无关事宜,使用完毕及时离开。

-

6.欢迎在《爱心母婴室使用意见簿》上留下您宝贵的意见和建议。

-
-
-

联系人:曾钰媛梦 84894774、15850692181

- - 已知晓并遵守 - -
- 场地预约 - 我的预约 @@ -308,37 +266,70 @@ layout("/mobile/platform.html"){
- +