diff --git a/src/main/java/io/v/nutz/sys/controllers/platform/sys/SysTaskController.java b/src/main/java/io/v/nutz/sys/controllers/platform/sys/SysTaskController.java index 4b72619..db02994 100644 --- a/src/main/java/io/v/nutz/sys/controllers/platform/sys/SysTaskController.java +++ b/src/main/java/io/v/nutz/sys/controllers/platform/sys/SysTaskController.java @@ -5,7 +5,7 @@ import io.v.nutz.sys.services.SysTaskService; import io.v.nutz.task.services.TaskPlatformService; import io.v.nutz.web.commons.slog.annotation.SLog; import io.v.nutz.base.utils.PageUtil; -import io.v.nutz.base.result.Result;; +import io.v.nutz.base.result.Result; import io.v.nutz.web.commons.utils.ShiroUtil; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.nutz.dao.Cnd; diff --git a/src/main/java/io/v/nutz/sys/services/SysUnitService.java b/src/main/java/io/v/nutz/sys/services/SysUnitService.java index caf9c6d..8952aa8 100644 --- a/src/main/java/io/v/nutz/sys/services/SysUnitService.java +++ b/src/main/java/io/v/nutz/sys/services/SysUnitService.java @@ -7,6 +7,12 @@ import io.v.nutz.sys.models.Sys_unit; * Created by wizzer on 2016/12/22. */ public interface SysUnitService extends ViService { + /** + * 无需参数,从数据中心同步全部单位,供单位维护及人员更新共同使用。 + * 返回 void;同步失败抛出异常,由调用接口返回失败提示。 + */ + void syncSourceUnits(); + /** * 保存单位 * diff --git a/src/main/java/io/v/nutz/sys/services/impl/SysUnitServiceImpl.java b/src/main/java/io/v/nutz/sys/services/impl/SysUnitServiceImpl.java index 5e8dc10..3ac8095 100644 --- a/src/main/java/io/v/nutz/sys/services/impl/SysUnitServiceImpl.java +++ b/src/main/java/io/v/nutz/sys/services/impl/SysUnitServiceImpl.java @@ -1,5 +1,11 @@ package io.v.nutz.sys.services.impl; +import cn.hutool.core.bean.BeanUtil; +import io.v.nutz.zhgh.data.constant.SourceData; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import io.v.nutz.base.constant.RedisConstant; import io.v.nutz.base.service.impl.ViServiceImpl; import io.v.nutz.sys.models.Sys_unit; @@ -25,6 +31,47 @@ public class SysUnitServiceImpl extends ViServiceImpl implements SysUn super(dao); } + /** + * 同步数据中心单位,无入参、无返回值;异常交由调用方处理。 + * 保留现有层级规则及根单位映射,重复同步按映射后的主键更新。 + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public void syncSourceUnits() { + List units = SourceData.units(); + Set ids = query().stream().map(Sys_unit::getId).collect(Collectors.toSet()); + for (Sys_unit unit : units) { + if (Strings.isBlank(unit.getId()) || Strings.isBlank(unit.getUnitcode())) { + throw new IllegalStateException("单位同步失败:源单位编号为空,请核对单位源数据。"); + } + // 源根单位 100 在本系统保存为 1,必须先映射再判断是否已存在。 + if ("100".equals(unit.getId())) { + unit.setId("1"); + unit.setUnitcode("1"); + unit.setUnitlevel(1); + } + if (ids.contains(unit.getId())) { + updateIgnoreNull(unit); + } else { + Map beanMap = BeanUtil.beanToMap(unit); + if (unit.getUnitcode().length() == 6) { + beanMap.put("unitlevel", 2); + beanMap.put("parentId", 1); + } + beanMap.remove("child"); + insert("sys_unit", Chain.from(beanMap)); + ids.add(unit.getId()); + } + } + // 沿用原入口对子单位标志的维护规则。 + for (Sys_unit unit : query()) { + if (count(Cnd.where("parentId", "=", unit.getId())) > 0) { + unit.setHasChildren(true); + update(unit); + } + } + } + /** * 新增单位 * diff --git a/src/main/java/io/v/nutz/zhgh/activity/services/impl/SiteInfoServiceImpl.java b/src/main/java/io/v/nutz/zhgh/activity/services/impl/SiteInfoServiceImpl.java index 43e3280..9d22ca6 100644 --- a/src/main/java/io/v/nutz/zhgh/activity/services/impl/SiteInfoServiceImpl.java +++ b/src/main/java/io/v/nutz/zhgh/activity/services/impl/SiteInfoServiceImpl.java @@ -36,7 +36,9 @@ public class SiteInfoServiceImpl extends BaseServiceImpl imple */ @Aop(TransAop.READ_COMMITTED) public void saveManagedSite(ActivitySiteInfo form, boolean editing) { - if (form == null || !"职工之家".equals(form.getName())) throw new IllegalArgumentException("场地名称请选择职工之家"); + // 名称支持选择或自定义输入,拒绝空白及超过实体字段100字符的值,类型关联仍沿用现有规则。 + if (form == null || Strings.isBlank(form.getName())) throw new IllegalArgumentException("请选择或输入场地名称"); + if (form.getName().length() > 100) throw new IllegalArgumentException("场地名称不能超过100字符"); if (Arrays.asList("青山湖科创中心", "不固定校区").contains(form.getCampus()) || Strings.isBlank(form.getCampus()) || form.getCampus().length() > 100 || dao().count("sys_dq", Cnd.where("dq_name", "=", form.getCampus())) == 0) throw new IllegalArgumentException("请选择有效校区"); diff --git a/src/main/java/io/v/nutz/zhgh/data/constant/SourceData.java b/src/main/java/io/v/nutz/zhgh/data/constant/SourceData.java index 81d2ada..2ea089c 100644 --- a/src/main/java/io/v/nutz/zhgh/data/constant/SourceData.java +++ b/src/main/java/io/v/nutz/zhgh/data/constant/SourceData.java @@ -146,33 +146,51 @@ public interface SourceData { }}; /** - * 获取单位 + * 分页获取全部源单位,无入参;响应异常或空页时抛出异常,阻止不完整同步。 * - * @return units + * @return 单位列表,DWH 映射为 id/unitcode,DWMC 为名称,SJDWH 为父级编号 */ static List units() { List units = new ArrayList<>(); int page = 1; - String body = HttpUtil.createPost(DATA_URL).body(JSON.toJSONString(new NutMap().addv("address", "rsxt_dwjbsj").addv("params", new NutMap()) - .addv("token", "3318a5a9-c2f2-4c8b-a8ea-e99dd68c165c").addv("pageIndex", page).addv("pageSize", 100))).execute().body(); - NutMap map = Json.fromJson(NutMap.class, body); -// checkSuccess(map); - List data = map.getAsList("data", NutMap.class); + while (true) { + String body = HttpUtil.createPost(DATA_URL).body(JSON.toJSONString(new NutMap().addv("address", "rsxt_dwjbsj").addv("params", new NutMap()) + .addv("token", "3318a5a9-c2f2-4c8b-a8ea-e99dd68c165c").addv("pageIndex", page).addv("pageSize", 100))).execute().body(); + NutMap map = Json.fromJson(NutMap.class, body); + // 缺少数据或明确返回失败时不能当作同步成功,避免用不完整单位继续更新人员。 + if (map == null || (map.containsKey("success") && !map.getBoolean("success")) + || !map.containsKey("data") || !map.containsKey("totalCount")) { + throw new IllegalStateException("单位同步失败:数据中心响应异常,请核对单位源接口。"); + } + List data = map.getAsList("data", NutMap.class); + int totalCount = map.getInt("totalCount"); + if (data == null || data.isEmpty() || totalCount <= 0) { + throw new IllegalStateException("单位同步失败:数据中心返回空页或总数异常,第 " + page + " 页。"); + } - for (NutMap row : data) { - Map entity = new HashMap(5); - row.forEach((k, v) -> { - if (UNIT_FIELD_RELATION.containsKey(k)) { - Object value = v; - /* if (UNIT_FIELD_PLUGIN.containsKey(k)) { - value = UNIT_FIELD_PLUGIN.get(k).run(v); - }*/ - for (String key : UNIT_FIELD_RELATION.get(k)) { - entity.put(key, value); - } + for (NutMap row : data) { + if (row == null) { + throw new IllegalStateException("单位同步失败:源单位记录为空。"); } - }); - units.add(BeanUtil.mapToBean(entity, Sys_unit.class, true)); + Map entity = new HashMap(5); + row.forEach((k, v) -> { + if (UNIT_FIELD_RELATION.containsKey(k)) { + Object value = v; + /* if (UNIT_FIELD_PLUGIN.containsKey(k)) { + value = UNIT_FIELD_PLUGIN.get(k).run(v); + }*/ + for (String key : UNIT_FIELD_RELATION.get(k)) { + entity.put(key, value); + } + } + }); + units.add(BeanUtil.mapToBean(entity, Sys_unit.class, true)); + } + // 根据源接口总数逐页拉取,避免原先只读取前 100 个单位。 + if (units.size() >= totalCount) { + break; + } + page++; } return units; } diff --git a/src/main/java/io/v/nutz/zhgh/data/controller/UpdateUnitController.java b/src/main/java/io/v/nutz/zhgh/data/controller/UpdateUnitController.java index 7366b30..c27e532 100644 --- a/src/main/java/io/v/nutz/zhgh/data/controller/UpdateUnitController.java +++ b/src/main/java/io/v/nutz/zhgh/data/controller/UpdateUnitController.java @@ -1,17 +1,17 @@ package io.v.nutz.zhgh.data.controller; -import cn.hutool.core.bean.BeanUtil; import cn.wizzer.framework.base.Result; import io.v.nutz.base.annontation.ViReturn; import io.v.nutz.base.dao.CndPlus; import io.v.nutz.base.query.PageForm; import io.v.nutz.base.service.AsyncService; -import io.v.nutz.base.service.ViService; +import io.v.nutz.sys.services.SysUnitService; import io.v.nutz.sys.models.Sys_unit; import io.v.nutz.sys.services.SysDqService; import io.v.nutz.sys.services.SysGxService; import io.v.nutz.sys.services.SysUnitClassService; -import io.v.nutz.zhgh.data.constant.SourceData; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.nutz.dao.Chain; import org.nutz.dao.Cnd; @@ -25,8 +25,6 @@ import org.nutz.mvc.annotation.Ok; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; @At("/platform/data/unit") @Ok("json:full") @@ -35,8 +33,8 @@ public class UpdateUnitController { private static final Log log = Logs.get(); - @Inject("Sys_unit") - private ViService sysUnitService; + @Inject + private SysUnitService sysUnitService; @Inject private SysUnitClassService sysUnitClassService; @@ -61,45 +59,16 @@ public class UpdateUnitController { /** * 单位数据更新 * - * @return {@link Object} + * 无入参,调用 service 同步单位;返回 null,由 ViReturn 转为 code/msg 响应。 + * 同步异常由 ViReturn 返回失败提示。 + * @return 同步成功返回 null */ @At @ViReturn @RequiresPermissions("sys.data.unit") + @Aop(TransAop.READ_COMMITTED) public Object dataUpdate() { - List sys_units = sysUnitService.query(); - List list = sys_units.stream().map(Sys_unit::getId).collect(Collectors.toList()); - - // 从源数据中心拉取所有的单位 - List units = SourceData.units(); - - for (Sys_unit unit : units) { - if (list.contains(unit.getId())) { - sysUnitService.updateIgnoreNull(unit); - } else { - Map beanMap = BeanUtil.beanToMap(unit); - String unitcode = beanMap.get("unitcode").toString(); - if (unitcode.length() == 6) { - beanMap.put("unitlevel", 2); - beanMap.put("parentId", 1); - } - if (beanMap.get("id").equals("100")) { - beanMap.put("unitlevel", 1); - beanMap.put("id", 1); - beanMap.put("unitcode", 1); - } - beanMap.remove("child"); - sysUnitService.insert("sys_unit", Chain.from(beanMap)); - } - } - - sys_units = sysUnitService.query(); - sys_units.forEach(v -> { - if (sysUnitService.count(Cnd.where("parentId", "=", v.getId())) > 0) { //查询此单位是否有父级单位 - v.setHasChildren(true); //设置子级菜单 - sysUnitService.update(v); - } - }); + sysUnitService.syncSourceUnits(); return null; } diff --git a/src/main/java/io/v/nutz/zhgh/data/service/impl/SourceUserServiceImpl.java b/src/main/java/io/v/nutz/zhgh/data/service/impl/SourceUserServiceImpl.java index 4f310eb..1ef4dc5 100644 --- a/src/main/java/io/v/nutz/zhgh/data/service/impl/SourceUserServiceImpl.java +++ b/src/main/java/io/v/nutz/zhgh/data/service/impl/SourceUserServiceImpl.java @@ -9,6 +9,8 @@ import cn.hutool.http.HtmlUtil; import io.v.nutz.base.utils.ManyAddOrRenewUtil; import io.v.nutz.base.utils.Roles; import io.v.nutz.sys.models.Sys_user_role; +import io.v.nutz.sys.models.Sys_unit; +import io.v.nutz.sys.services.SysUnitService; import io.v.nutz.sys.services.SysDictService; import io.v.nutz.sys.services.SysUserRoleService; import io.v.nutz.zhgh.activity.models.ActivityUserScope; @@ -83,6 +85,8 @@ public class SourceUserServiceImpl extends ViServiceImpl implements @Inject private SysUserService sysUserService; @Inject + private SysUnitService sysUnitService; + @Inject private HistoryUserService historyUserService; @Inject private UserPartUpService userPatUpService; @@ -109,10 +113,22 @@ public class SourceUserServiceImpl extends ViServiceImpl implements dao().insert(users); } + /** + * 先同步单位并校验人员引用,再按现有配置生成记录和更新人员。 + * @param pullTime 已拉取人员数据的批次时间 + * @param sourceType all 全部更新、add 新增人员、part 按组别及过滤字段更新 + * @param columnNames 部分更新时忽略的字段名,可为空 + * @param isUpSet 保留现有接口参数,当前方法不使用 + * @param partGroupId 更新组别 ID,为空时使用整个批次 + * @param isInvert 是否选择组别之外的人员 + * @return 无返回值;同步或校验失败抛出异常,由接口返回 code/msg 提示 + */ @Override @Aop(TransAop.READ_COMMITTED) public void updateSysUser(String pullTime, String sourceType, String[] columnNames, String isUpSet, String partGroupId, boolean isInvert) { try { + // 单位与后续校验使用当前事务,单位同步失败时不进入人员更新。 + sysUnitService.syncSourceUnits(); List sources = new ArrayList<>(); Map userPartMap = new HashMap<>(); if (Strings.isNotBlank(partGroupId)) { @@ -172,104 +188,89 @@ public class SourceUserServiceImpl extends ViServiceImpl implements Set userIds = list.stream().map(SpecialStaff::getUserId).collect(Collectors.toSet()); - // 创建一个固定大小的线程池 可以根据服务器性能调整线程池大小 - int numberOfThreads = Runtime.getRuntime().availableProcessors() * 2; - List> futures = new CopyOnWriteArrayList<>(); - // 每批处理的数据量,可以根据实际情况调整 - int batchSize = 200; + // 写入人员、角色、历史之前统一校验;记录计算留在当前线程,读取本事务新同步的单位。 + validateSourceUnits(sources, userMap, userIds, allowChangeFieldNames); + for (UserSource source : sources) { + //不更新会员和福利会员字段 + source.setMember(null); + source.setWelfareMember(null); + Sys_user user = userMap.get(source.getLoginname()); - for (int i = 0; i < sources.size(); i += batchSize) { - final int end = Math.min(i + batchSize, sources.size()); - List batch = sources.subList(i, end); - Map finalUserPartMap = userPartMap; + if (user != null && userIds.contains(user.getId())) { + continue; + } - CompletableFuture future = CompletableFuture.runAsync(() -> { - for (UserSource source : batch) { - //不更新会员和福利会员字段 - source.setMember(null); - source.setWelfareMember(null); - Sys_user user = userMap.get(source.getLoginname()); + if (StrUtil.isBlank(source.getPersonType())) { + continue; + } - if (user != null && userIds.contains(user.getId())) { - continue; - } + // 这里是杭医的不进系统人员的判断,其他学校用不到的话删掉 + boolean isNotEnterUser = NOT_ENTERING_PERSON_TYPE.contains(source.getPersonType()) + || NOT_ENTERING_USER_LOGIN_NAME.contains(source.getLoginname()) + || NOT_ENTERING_USER_STATE.contains(source.getUserState()); - if (StrUtil.isBlank(source.getPersonType())) { - continue; - } + if (isAuto) { + checkMemberOrWelfareMember(changeConfig, source, user, addMemberUserIds, deleteMemberUserIds, + addWelfareMemberUserIds, deleteWelfareMemberUserIds); + } else { + if (user != null) { + source.setMember(user.getMember()); + source.setWelfareMember(user.getWelfareMember()); + } + } - // 这里是杭医的不进系统人员的判断,其他学校用不到的话删掉 - boolean isNotEnterUser = NOT_ENTERING_PERSON_TYPE.contains(source.getPersonType()) - || NOT_ENTERING_USER_LOGIN_NAME.contains(source.getLoginname()) - || NOT_ENTERING_USER_STATE.contains(source.getUserState()); - - if (isAuto) { - checkMemberOrWelfareMember(changeConfig, source, user, addMemberUserIds, deleteMemberUserIds, - addWelfareMemberUserIds, deleteWelfareMemberUserIds); - } else { - if (user != null) { - source.setMember(user.getMember()); - source.setWelfareMember(user.getWelfareMember()); - } - } - - Sys_user u = new Sys_user(); - BeanUtils.copyProperties(source, u); - switch (sourceType) { - case "all" -> { - // 复制属性到一个新的user对象 - u.setId(user == null ? source.getId() : user.getId()); - if (user != null) { - needDoUpdateList.add(u); - } - } - case "add" -> { - u.setId(source.getId()); - } - case "part" -> { - u.setId(user == null ? source.getId() : user.getId()); - if (columnNames != null && columnNames.length > 0) { - String lockedColumn = String.join("|", columnNames); - FieldFilter fieldFilter = FieldFilter.create(Sys_user.class, null, "^" + lockedColumn + "$", true); - filterColumnDao.set(Daos.ext(dao(), fieldFilter)); - } - if (Strings.isNotBlank(finalUserPartMap.get(u.getLoginname())) && user != null) { - needDoUpdateList.add(u); - } - } - } - - if (isAuto) { - UserHistory history = createHistory(source, user, dictMap, allowChangeFieldNames); - if (Lang.isEmpty(history)) { - continue; - } - if (user != null) { - histories.add(history); - } - if (!isNotEnterUser && Lang.isNotEmpty(history.getChangeTypes()) && history.getChangeTypes().contains(MemberChangeType.NEW.getType())) { - needInitUserList.add(u); - histories.add(history); - } - } else { - SourceChangeMiddleTable table = createSourceChangeMiddleTable(source, user, dictMap, allowChangeFieldNames); - if (Lang.isEmpty(table)) { - continue; - } - if (user != null) { - middleTables.add(table); - } - if (!isNotEnterUser && Lang.isNotEmpty(table.getChangeTypes()) && table.getChangeTypes().contains(MemberChangeType.NEW.getType())) { - needInitUserList.add(u); - middleTables.add(table); - } + Sys_user u = new Sys_user(); + BeanUtils.copyProperties(source, u); + switch (sourceType) { + case "all" -> { + // 复制属性到一个新的user对象 + u.setId(user == null ? source.getId() : user.getId()); + if (user != null) { + needDoUpdateList.add(u); } } - }, Executors.newFixedThreadPool(numberOfThreads)); - futures.add(future); - } + case "add" -> { + u.setId(source.getId()); + } + case "part" -> { + u.setId(user == null ? source.getId() : user.getId()); + if (columnNames != null && columnNames.length > 0) { + String lockedColumn = String.join("|", columnNames); + FieldFilter fieldFilter = FieldFilter.create(Sys_user.class, null, "^" + lockedColumn + "$", true); + filterColumnDao.set(Daos.ext(dao(), fieldFilter)); + } + if (Strings.isNotBlank(userPartMap.get(u.getLoginname())) && user != null) { + needDoUpdateList.add(u); + } + } + } - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + if (isAuto) { + UserHistory history = createHistory(source, user, dictMap, allowChangeFieldNames); + if (Lang.isEmpty(history)) { + continue; + } + if (user != null) { + histories.add(history); + } + if (!isNotEnterUser && Lang.isNotEmpty(history.getChangeTypes()) && history.getChangeTypes().contains(MemberChangeType.NEW.getType())) { + needInitUserList.add(u); + histories.add(history); + } + } else { + SourceChangeMiddleTable table = createSourceChangeMiddleTable(source, user, dictMap, allowChangeFieldNames); + if (Lang.isEmpty(table)) { + continue; + } + if (user != null) { + middleTables.add(table); + } + if (!isNotEnterUser && Lang.isNotEmpty(table.getChangeTypes()) && table.getChangeTypes().contains(MemberChangeType.NEW.getType())) { + needInitUserList.add(u); + middleTables.add(table); + } + } + } //如果有新用户,增加到用户表同时增加角色 if (Lang.isNotEmpty(needInitUserList)) { @@ -322,11 +323,69 @@ public class SourceUserServiceImpl extends ViServiceImpl implements } catch (Exception e) { e.printStackTrace(); - throw new RuntimeException(e.getMessage()); + throw new RuntimeException(e.getMessage(), e); } } + /** + * 校验本次范围内参与新增、更新或变更记录生成的单位,失败时阻断全部人员写入。 + * @param sources 已按数据源时间、组别及反选条件筛选的人员 + * @param userMap 以工号为键的现有人员,用于检查变更前单位 + * @param excludedUserIds 按现有特殊人员规则不做变动的人员 ID + * @param allowChangeFieldNames 允许记录变更的字段,含 unitId 时核对变更前单位 + * @return 无返回值;缺失时抛出包含单位编号、姓名和工号的异常,供接口 msg 展示 + */ + private void validateSourceUnits(List sources, Map userMap, + Set excludedUserIds, Set allowChangeFieldNames) { + Map units = sysUnitService.query().stream() + .collect(Collectors.toMap(Sys_unit::getId, unit -> unit)); + List missing = new ArrayList<>(); + for (UserSource source : sources) { + Sys_user user = userMap.get(source.getLoginname()); + if (StrUtil.isBlank(source.getPersonType()) || (user != null && excludedUserIds.contains(user.getId()))) { + continue; + } + // 不进系统规则只排除新增人员;现有人员仍按原逻辑生成变更记录。 + if (user == null && (NOT_ENTERING_PERSON_TYPE.contains(source.getPersonType()) + || NOT_ENTERING_USER_LOGIN_NAME.contains(source.getLoginname()) + || (source.getUserState() != null && NOT_ENTERING_USER_STATE.contains(source.getUserState())))) { + continue; + } + collectMissingUnit(units, source.getUnitid(), "新单位", source, missing); + if (user != null && allowChangeFieldNames.contains("unitId") + && !Objects.equals(source.getUnitid(), user.getUnitid())) { + collectMissingUnit(units, user.getUnitid(), "原单位", source, missing); + } + } + if (!missing.isEmpty()) { + // 页面提示限制长度,同时告知总数;完整明细写日志便于核对。 + log.warn("人员更新单位校验失败:{}", String.join(";", missing)); + throw new IllegalStateException("单位同步校验后仍存在缺失单位或单位名称为空,本次人员更新已停止。" + + String.join(";", missing.subList(0, Math.min(10, missing.size()))) + + (missing.size() > 10 ? ";共 " + missing.size() + " 项,完整明细请查看服务日志" : "") + + "。请核对单位源数据。"); + } + } + + /** + * 将不存在、编号为空或名称为空的单位加入提示明细;仅收集问题,不修改数据。 + * @param units 当前事务同步后的单位,键为单位 ID + * @param unitId 人员引用的单位 ID,空值也需提示 + * @param label 新单位或原单位,标明问题来源 + * @param source 相关人员,提供姓名及工号 + * @param missing 校验失败明细,返回结果追加到此集合;方法返回 void + */ + private void collectMissingUnit(Map units, String unitId, String label, + UserSource source, List missing) { + Sys_unit unit = units.get(unitId); + if (StrUtil.isBlank(unitId) || unit == null || StrUtil.isBlank(unit.getName())) { + missing.add(label + "编号:" + Strings.sBlank(unitId, "未提供") + + ",相关人员:" + Strings.sBlank(source.getUsername(), "未提供姓名") + + "(工号:" + Strings.sBlank(source.getLoginname(), "未提供") + ")"); + } + } + /** * 判断自动更新的会员和福利会员 * @param config 人员更新的配置 @@ -553,8 +612,9 @@ public class SourceUserServiceImpl extends ViServiceImpl implements if (Lang.isEmpty(changeList)) { return null; } + // 单位缺失由前置校验阻断,其他字段的空值按原业务展示为无数据。 String changeInfos = changeList.stream().map(v -> { - return v.getString("fieldName") + ":" + HtmlUtil.cleanHtmlTag(v.getString("sourceValue")) + "—>" + HtmlUtil.cleanHtmlTag(v.getString("newValue")); + return v.getString("fieldName") + ":" + HtmlUtil.cleanHtmlTag(Strings.sBlank(v.getString("sourceValue"), "无数据")) + "—>" + HtmlUtil.cleanHtmlTag(Strings.sBlank(v.getString("newValue"), "无数据")); }).collect(Collectors.joining(";")); // 比较变更数据 @@ -609,8 +669,9 @@ public class SourceUserServiceImpl extends ViServiceImpl implements if (Lang.isEmpty(changeList)) { return null; } + // 单位缺失由前置校验阻断,其他字段的空值按原业务展示为无数据。 String changeInfos = changeList.stream().map(v -> { - return v.getString("fieldName") + ":" + HtmlUtil.cleanHtmlTag(v.getString("sourceValue")) + "—>" + HtmlUtil.cleanHtmlTag(v.getString("newValue")); + return v.getString("fieldName") + ":" + HtmlUtil.cleanHtmlTag(Strings.sBlank(v.getString("sourceValue"), "无数据")) + "—>" + HtmlUtil.cleanHtmlTag(Strings.sBlank(v.getString("newValue"), "无数据")); }).collect(Collectors.joining(";")); // 比较变更数据 commonCompareChange(source, user, changeTypes); diff --git a/src/main/resources/views/mobile/activity/site/reserve.html b/src/main/resources/views/mobile/activity/site/reserve.html index 68beebb..0e174a0 100644 --- a/src/main/resources/views/mobile/activity/site/reserve.html +++ b/src/main/resources/views/mobile/activity/site/reserve.html @@ -24,7 +24,7 @@ layout("/mobile/platform.html"){ } .site-reserve-slots .time { - width: 50%; + display: block; text-align: center; white-space: nowrap; } @@ -32,23 +32,19 @@ layout("/mobile/platform.html"){ /* 长禁用原因由内容撑开行高,避免固定高度导致文字覆盖下一场次。 */ .site-reserve-slots .state { width: 100%; - min-height: 36px; - box-sizing: border-box; - display: flex; - align-items: center; - justify-content: center; - padding: 6px 8px; - line-height: 20px; + display: block; + line-height: 18px; white-space: normal; overflow-wrap: anywhere; text-align: center; - background-color: #f2f2f2; - border-radius: 2px; - color: #929292; + color: #1989fa; } .site-reserve-slots { - padding: 10px 0px; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px 16px; + padding: 8px 0 66px; background-color: white; width: 95%; margin: 0 auto; @@ -56,18 +52,53 @@ layout("/mobile/platform.html"){ margin-top: 10px; } - .site-reserve-slots table { - width: 100%; - table-layout: fixed; - border-collapse: separate; - border-spacing: 0 4px; + /* 单个按钮容纳时间和状态,选中仅改变外观,继续复用原有时段选择逻辑。 */ + .site-reserve-slots .slot-button { + min-width: 0; + min-height: 54px; + box-sizing: border-box; + padding: 8px 6px; + border: 1px solid #e6e8ed; + border-radius: 12px; + background: #fff; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04); + color: #323233; + font: inherit; + font-size: 13px; + line-height: 18px; + text-align: center; + cursor: pointer; } - .site-reserve-slots td { - padding: 0 6px; - vertical-align: middle; + .site-reserve-slots .slot-button.is-selected { + background: #ecf6ff; + border-color: #a4d2ff; + color: #1989fa; + box-shadow: 0 4px 12px rgba(25, 137, 250, 0.08); } + .site-reserve-slots .slot-button:disabled, + .site-reserve-slots .slot-button:disabled .state { + color: #969799; + background: #f7f8fa; + box-shadow: none; + cursor: default; + } + /* 全天候四列时间点:网格单元承载连续底色,按钮端点保留圆角及起终角标。 */ + .site-reserve-slots.full-day-slots { grid-template-columns:repeat(4,minmax(0,1fr)); gap:18px 0; } + .full-day-slots .point-cell { min-width:0; padding:0 8px; display:flex; } + .full-day-slots .point-cell.in-range { background:#e2f1ff; padding:0; } + .full-day-slots .point-cell.range-start { padding-left:8px; background:linear-gradient(to right,#fff 8px,#e2f1ff 8px); } + .full-day-slots .point-cell.range-end { padding-right:8px; background:linear-gradient(to left,#fff 8px,#e2f1ff 8px); } + .full-day-slots .slot-button { width:100%; position:relative; padding:8px 2px; } + .full-day-slots .in-range .slot-button { border-radius:0; background:#e2f1ff; border-color:transparent; box-shadow:none; } + .full-day-slots .range-start .slot-button { border-radius:12px 0 0 12px; } + .full-day-slots .range-end .slot-button { border-radius:0 12px 12px 0; } + .full-day-slots .point-cell .range-boundary { border-color:#a4d2ff; color:#1989fa; background:#ecf6ff; } + .full-day-slots .range-mark { position:absolute; color:#fff; background:#1989fa; font-size:11px; line-height:18px; padding:0 4px; } + .full-day-slots .range-mark.start { top:0; left:0; border-radius:10px 0 5px 0; } + .full-day-slots .range-mark.end { bottom:0; right:0; border-radius:5px 0 10px 0; } + .van-action-sheet__content { padding: 10px; font-size: 15px; @@ -195,27 +226,28 @@ layout("/mobile/platform.html"){ -
- - - - - - -
{{item.start_time + '-' + item.end_time}} -
- - {{item.msg === '预约时段已过期' ? '已过期' : item.msg}} -
-
-
- {{item.msg}} - - - -
-
+
+
+ +
+
+
+
@@ -230,7 +262,7 @@ layout("/mobile/platform.html"){ {{unit}} {{phone}} -
+
{{time.fullDay}} @@ -326,6 +358,8 @@ layout("/mobile/platform.html"){ timeData: [], slotRequestVersion:0, slotsLoading:false, times: [], + // 每个日期独立保存起止边界;times仍保存原接口要求的逐场次记录。 + fullDayRanges: {}, slotsFailed:false, weekList: [], joinUser: '', calendarVisible: false, @@ -337,6 +371,27 @@ layout("/mobile/platform.html"){ reserve_type() { this.getReserve() } }, computed: { + isFullDay() { return Number(this.site.reserveTimeType || this.site.reservetimetype || 1) === 2 }, + currentRange() { return this.fullDayRanges[this.selectDate.format('YYYY-MM-DD')] || {start:'',end:''} }, + // 结束点包括最后一个场次的结束边界,24:00保留原字符串,不转换为次日00:00。 + fullDayPoints() { + const range=this.currentRange, choosingEnd=range.start && !range.end + return Array.from(new Set(this.timeData.flatMap(row=>[row.start_time,row.end_time]))).sort().map(time=>{ + const row=this.timeData.find(slot=>slot.start_time===time) + const selected=time===range.start || !!range.end && time>=range.start && time<=range.end + const validEnd=choosingEnd && this.fullDaySlots(range.start,time) + const disabled=!selected && (choosingEnd ? !validEnd : !row || row.code!==1) + const reason=disabled ? choosingEnd ? '不可选' : row ? row.msg==='预约时段已过期' ? '已过期' : row.msg : '仅结束时间' : '可预约' + return {time,selected,disabled,reason,inRange:!!range.end && selected,mark:time===range.start ? '起' : time===range.end ? '终' : ''} + }) + }, + confirmationTimes() { + if (!this.isFullDay) return this.times + return Object.keys(this.fullDayRanges).sort().filter(day=>this.fullDayRanges[day].end).map(day=>{ + const range=this.fullDayRanges[day] + return {day,fullDay:day+' '+range.start+'-'+range.end} + }) + }, reserveTypeName() { // 未选择时显示提示,避免读取不存在的选项名称。 const option = this.reserveTypeOptions.find(item => item.value===this.reserve_type) @@ -399,6 +454,39 @@ layout("/mobile/platform.html"){ this.$set(this,'slotRequestVersion',this.slotRequestVersion+1) }, methods: { + // start/end为HH:mm边界,返回全部连续可约场次;空范围、禁用或断档返回null。 + fullDaySlots(start,end) { + if (!start || !end || end<=start) return null + const rows=this.timeData.filter(row=>row.start_time>=start && row.end_time<=end).slice().sort((a,b)=>a.start_time.localeCompare(b.start_time)) + if (!rows.length || rows[0].start_time!==start || rows[rows.length-1].end_time!==end || rows.some((row,i)=>row.code!==1 || i>0 && rows[i-1].end_time!==row.start_time)) return null + return rows + }, + // 仅清除指定日期的完整范围及提交记录,其他日期的选择保持不变。 + clearFullDay(day) { + this.$set(this.fullDayRanges,day,{start:'',end:''}) + this.$set(this,'times',this.times.filter(row=>row.day!==day)) + }, + chooseFullDayPoint(point) { + if (this.slotsLoading || this.formLoading || point.disabled) return + const day=this.selectDate.format('YYYY-MM-DD'),range=this.currentRange + if (point.selected) { this.clearFullDay(day); return } + if (!range.start || range.end) { + this.clearFullDay(day) + this.$set(this.fullDayRanges,day,{start:point.time,end:''}) + return + } + const rows=this.fullDaySlots(range.start,point.time) + if (!rows) return + this.$set(this.fullDayRanges,day,{start:range.start,end:point.time}) + this.$set(this,'times',this.times.filter(row=>row.day!==day).concat(rows.map(row=>Object.assign({},row,{day,fullDay:day+' '+row.start_time+'-'+row.end_time})))) + }, + // 任一日期仅选起点都不能确认,避免跨日切换后漏提交未完成的范围。 + validateFullDay() { + if (this.isFullDay && Object.values(this.fullDayRanges).some(range=>range.start && !range.end)) { + vant.Toast('请选择结束时间或取消未完成的选择'); return false + } + return true + }, // 与PC共用所属分工会接口,页面销毁后的响应不再更新界面。 loadBookingUnion() { this.$set(this,'unionLoading',true) @@ -437,6 +525,11 @@ layout("/mobile/platform.html"){ else this.clearPopupHistory(() => pjaxReplace('/mobile/activity/site/info')) }, close(f) { + if (this.isFullDay) { + if (this.confirmationTimes.length===1) { vant.Toast('必须保留一个时间段'); return } + this.clearFullDay(f.split(' ')[0]) + return + } if (this.times.length === 1) { vant.Toast('必须保留一个时间段') return @@ -447,6 +540,7 @@ layout("/mobile/platform.html"){ onConfirm(date) { this.selectDate = moment(date) this.setWeekList(this.selectDate) + this.getReserve() this.calendarVisible = false }, //这里要排除掉休息日 @@ -469,6 +563,7 @@ layout("/mobile/platform.html"){ this.getReserve() }, reserve() { + if (this.slotsLoading || this.slotsFailed || !this.validateFullDay()) return if (this.times.length === 0) { vant.Toast('请选择要预约的时段') return @@ -477,8 +572,9 @@ layout("/mobile/platform.html"){ }, reserveDo() { if(this.slotsLoading || this.formLoading)return + if(this.slotsFailed || !this.validateFullDay())return if(!this.times.length){vant.Toast('请选择预约时段');return} - if(Number(this.site.reserveTimeType || 1)===2){ + if(this.isFullDay){ const days=Array.from(new Set(this.times.map(row=>row.day))) const gaps=days.some(day=>{ const selected=this.times.filter(row=>row.day===day).slice().sort((a,b)=>a.start_time.localeCompare(b.start_time)) @@ -492,7 +588,7 @@ layout("/mobile/platform.html"){ if(this.reserve_type===3 && !this.clubId){vant.Toast('请选择所属协会');return} this.formLoading=true; $.post('/mobile/activity/site/info/reserveDo',{siteId:this.site_id,times:JSON.stringify(this.times),message:this.message,reserve_type:this.reserve_type,clubId:this.clubId}).then((res)=>{ - if(res.code===0){this.clearPopupHistory(()=>{vant.Toast('预约成功');this.times=[];this.message='';this.getReserve();this.onLoad()})} + if(res.code===0){this.clearPopupHistory(()=>{vant.Toast('预约成功');this.times=[];this.$set(this,'fullDayRanges',{});this.message='';this.getReserve();this.onLoad()})} else{vant.Toast(res.msg)} }).always(()=>{this.formLoading=false}) }, @@ -519,13 +615,23 @@ layout("/mobile/platform.html"){ const version=this.slotRequestVersion+1 this.$set(this,'slotRequestVersion',version) this.$set(this,'slotsLoading',true) + this.$set(this,'slotsFailed',false) this.$set(this,'timeData',[]) // 未选类型时仍按团体预约规则查询占用,仅用于场次查询,不回填表单类型。 return $.post('/mobile/activity/site/info/getReserve',{siteId:this.site_id,day:moment(this.selectDate).format('YYYY-MM-DD'),reserveType:this.reserve_type===null ? 2 : this.reserve_type}).then(res=>{ if(version!==this.slotRequestVersion)return - if(res && res.code===0)this.$set(this,'timeData',res.data) - else vant.Toast(res && res.msg || '场次加载失败') - },()=>{if(version===this.slotRequestVersion)vant.Toast('场次加载失败,请重试')}).always(()=>{ + if(res && res.code===0 && Array.isArray(res.data)) { + this.$set(this,'timeData',res.data) + // 切换日期或类型后重新核验该日范围,保留其他日期;占用变化时整段清空。 + if (this.isFullDay) { + const range=this.currentRange + if (range.start && (range.end ? !this.fullDaySlots(range.start,range.end) : !res.data.some(row=>row.start_time===range.start && row.code===1))) { + this.clearFullDay(this.selectDate.format('YYYY-MM-DD')) + vant.Toast('所选时间已失效,请重新选择') + } + } + } else { this.$set(this,'slotsFailed',true); vant.Toast(res && res.msg || '场次加载失败') } + },()=>{if(version===this.slotRequestVersion){this.$set(this,'slotsFailed',true);vant.Toast('场次加载失败,请重试')}}).always(()=>{ if(version===this.slotRequestVersion)this.$set(this,'slotsLoading',false) }) }, diff --git a/src/main/resources/views/platform/activity/site/SiteManage.html b/src/main/resources/views/platform/activity/site/SiteManage.html index 9810bb0..807e7d6 100644 --- a/src/main/resources/views/platform/activity/site/SiteManage.html +++ b/src/main/resources/views/platform/activity/site/SiteManage.html @@ -137,7 +137,9 @@ layout("/layouts/platform.html"){ - + + @@ -319,7 +321,11 @@ layout("/layouts/platform.html"){ year: "" }, formRules: { - name: [{required: true, message: '请选择场地名称', trigger: ['blur', 'change']}], + // 自定义名称与后端保持一致:必填、拒绝全空格且不超过实体字段的100字符。 + name: [ + {required: true, whitespace: true, message: '请选择或输入场地名称', trigger: ['blur', 'change']}, + {max: 100, message: '场地名称不能超过100字符', trigger: ['blur', 'change']} + ], address: [{required: true, message: '请填写场地地址', trigger: ['blur', 'change']}], contact_person: [{required: true, message: '请填写场地管理员', trigger: ['blur', 'change']}], contact_phone: [{required: true, message: '请填写联系电话', trigger: ['blur', 'change']}], @@ -422,9 +428,9 @@ layout("/layouts/platform.html"){ this.findOneSite(id).then((row)=>{if(row){this.initSchedule(row);this.$set(this, 'formData', row);this.$refs.guava.edit()}}); }, openAdd() { - // 新建只提供职工之家;校区由用户明确选择,不沿用上次编辑值。 + // 新建时名称留空,由用户选择或输入;校区由用户明确选择,不沿用上次编辑值。 this.$set(this, 'formData', { - name: '职工之家', + name: '', campus: '', state: true, multiple: true, diff --git a/src/main/resources/views/platform/activity/site/SiteReserve.html b/src/main/resources/views/platform/activity/site/SiteReserve.html index 4fa0b34..b930679 100644 --- a/src/main/resources/views/platform/activity/site/SiteReserve.html +++ b/src/main/resources/views/platform/activity/site/SiteReserve.html @@ -109,6 +109,17 @@ layout("/layouts/platform.html"){ .site-booking-date { flex:0 0 88px; white-space:nowrap; font-weight:500; } .site-booking-ranges { display:flex; flex-wrap:wrap; gap:2px 12px; } .site-booking-range { white-space:nowrap; } +/* 时间选择区平铺全部选项,日期和按钮自动换行;禁用原因仅通过悬停提示展示。 */ +.site-time-dates { display:flex; flex-wrap:wrap; gap:8px; margin:12px 0 20px; } +.site-time-group { display:flex; gap:16px; margin:20px 0; align-items:flex-start; } +.site-time-heading { flex:0 0 140px; line-height:36px; color:#606266; white-space:nowrap; } +.site-time-options { display:flex; flex-wrap:wrap; gap:8px; flex:1; } +.site-time-options .el-button { margin:0; position:relative; min-width:82px; min-height:36px; } +.site-time-options .is-range { background:#ecf5ff; border-color:#409eff; color:#1672bd; } +.site-time-mark { position:absolute; top:2px; right:3px; font-size:10px; line-height:12px; font-weight:bold; } +.site-time-summary { margin:16px 0; color:#303133; line-height:24px; } +.site-time-help { color:#909399; line-height:24px; } +@media (max-width:800px) { .site-time-group { flex-direction:column; gap:4px; } .site-time-heading { flex-basis:auto; } }
@@ -320,7 +331,7 @@ layout("/layouts/platform.html"){ 您正在预约【{{infoViewData.name}}】活动场地
- 开放时间段为: + 开放时间段为: 工作日 【{{item.start_time}} - {{item.end_time}}】 @@ -368,7 +379,32 @@ layout("/layouts/platform.html"){ - + + +
已选择的预约日期
+
{{day}}
+
所有选中日期使用同一组时间,只有全部日期均可预约的时间才能选择。
+
{{isFullDayBooking ? '先点击开始时间,再点击结束时间;再次点击已选时间可取消本次选择。' : '点击时间段选择,再次点击取消,可选择多个时间段。'}}
+
+
重新加载
+
暂无开放时段
+
+
{{group.label}}
+
+ + {{item.label}}{{item.mark}} + +
+
+
+
已选时间:{{bookingTimeSummary}}
+ 清空选择 +
+ +
{{day}}
@@ -427,13 +463,8 @@ layout("/layouts/platform.html"){ --> - - - + {{bookingTimeSummary}} + 修改时间