فهرست منبع

fix: reconcile field access permissions

Sheep 14 ساعت پیش
والد
کامیت
427d52d0d0

+ 332 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/service/FieldAccessPermissionService.java

@@ -0,0 +1,332 @@
+package org.jeecg.modules.app.service;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.jeecg.common.constant.CommonConstant;
+import org.jeecg.common.exception.JeecgBootException;
+import org.jeecg.modules.system.app.entity.AppDevice;
+import org.jeecg.modules.system.app.entity.AppOrderProInfo;
+import org.jeecg.modules.system.app.entity.AppSitePriceRules;
+import org.jeecg.modules.system.app.entity.FamilyMembers;
+import org.jeecg.modules.system.app.mapper.AppDeviceMapper;
+import org.jeecg.modules.system.app.mapper.AppOrderProInfoMapper;
+import org.jeecg.modules.system.app.mapper.FamilyMembersMapper;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import static org.jeecg.modules.hikiot.HikiotTool.addFace;
+import static org.jeecg.modules.hikiot.HikiotTool.addUser;
+
+/**
+ * Keeps field-order access permissions consistent between the order database
+ * and Hikvision devices.
+ */
+@Slf4j
+@Service
+public class FieldAccessPermissionService {
+
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
+    private static final int MAX_HIKIOT_ATTEMPTS = 3;
+
+    @Resource
+    private AppOrderProInfoMapper appOrderProInfoMapper;
+    @Resource
+    private FamilyMembersMapper familyMembersMapper;
+    @Resource
+    private AppDeviceMapper appDeviceMapper;
+    @Resource
+    private FaceImageNormalizationService faceImageNormalizationService;
+    @Resource
+    @Qualifier("ioIntensiveTaskExecutor")
+    private ThreadPoolTaskExecutor ioIntensiveTaskExecutor;
+
+    /**
+     * Rebuilds one person's permission for one site. Pending orders and a
+     * same-day used order are both retained so a successful entry never removes
+     * the permission needed to leave the school.
+     */
+    public void synchronizeFamilySite(String familyId, String siteId) {
+        if (StringUtils.isBlank(familyId) || StringUtils.isBlank(siteId)) {
+            throw new JeecgBootException("门禁权限同步参数不完整");
+        }
+
+        List<AppOrderProInfo> activeOrders = appOrderProInfoMapper.selectList(
+                Wrappers.<AppOrderProInfo>lambdaQuery()
+                        .eq(AppOrderProInfo::getFamilyUserId, familyId)
+                        .eq(AppOrderProInfo::getSiteId, siteId)
+                        .eq(AppOrderProInfo::getType, CommonConstant.ORDER_PRO_INFO_TYPE_0)
+                        .in(AppOrderProInfo::getOrderStatus,
+                                CommonConstant.ORDER_STATUS_1, CommonConstant.ORDER_STATUS_2)
+                        .ge(AppOrderProInfo::getUseDateStr, LocalDate.now().format(DATE_FORMATTER))
+                        .isNotNull(AppOrderProInfo::getUseDateStr)
+                        .isNotNull(AppOrderProInfo::getFrameTimeStr));
+        AccessWindow accessWindow = buildAccessWindow(activeOrders);
+        if (accessWindow == null) {
+            throw new JeecgBootException("未找到有效的场地订单门禁时段");
+        }
+
+        FamilyMembers familyMember = familyMembersMapper.selectById(familyId);
+        if (familyMember == null) {
+            throw new JeecgBootException("订单使用人不存在");
+        }
+        String faceUrl = faceImageNormalizationService.ensureNormalized(familyMember);
+        List<AppDevice> devices = appDeviceMapper.selectList(
+                Wrappers.<AppDevice>lambdaQuery().eq(AppDevice::getSiteId, siteId));
+        if (CollUtil.isEmpty(devices)) {
+            throw new JeecgBootException("场地未配置门禁设备,请联系管理员");
+        }
+
+        Date beginDate = Date.from(accessWindow.getBeginDate().atTime(12, 0)
+                .atZone(ZoneId.systemDefault()).toInstant());
+        Date endDate = Date.from(accessWindow.getEndDate().atTime(12, 0)
+                .atZone(ZoneId.systemDefault()).toInstant());
+        String frameTime = accessWindow.getBeginTime().format(TIME_FORMATTER)
+                + "-" + accessWindow.getEndTime().format(TIME_FORMATTER);
+
+        for (AppDevice device : devices) {
+            if (device == null || StringUtils.isBlank(device.getDeviceSerial())) {
+                continue;
+            }
+            String serial = device.getDeviceSerial();
+            invokeHikiotWithRetry("用户权限", serial,
+                    () -> addUser(beginDate, serial, familyMember.getFullName(), familyId, endDate, frameTime));
+            invokeHikiotWithRetry("人脸", serial, () -> addFace(serial, familyId, faceUrl));
+        }
+        log.info("场地门禁权限同步完成,siteId={}, familyId={}, beginDate={}, endDate={}, frameTime={}, deviceCount={}",
+                siteId, familyId, accessWindow.getBeginDate(), accessWindow.getEndDate(), frameTime, devices.size());
+    }
+
+    /**
+     * Applies changed school opening hours to already-created active orders and
+     * republishes affected permissions after the surrounding transaction commits.
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public int updateFutureOrderSlotsAndQueue(String siteId, Collection<AppSitePriceRules> priceRules) {
+        if (StringUtils.isBlank(siteId) || CollUtil.isEmpty(priceRules)) {
+            return 0;
+        }
+
+        Map<String, List<String>> framesByDate = new HashMap<>();
+        for (AppSitePriceRules rule : priceRules) {
+            if (rule == null || rule.getDateOfSale() == null || rule.getStartTime() == null || rule.getEndTime() == null) {
+                continue;
+            }
+            String date = rule.getDateOfSale().toInstant().atZone(ZoneId.systemDefault())
+                    .toLocalDate().format(DATE_FORMATTER);
+            String frame = rule.getStartTime().toInstant().atZone(ZoneId.systemDefault())
+                    .toLocalTime().format(TIME_FORMATTER)
+                    + "-" + rule.getEndTime().toInstant().atZone(ZoneId.systemDefault())
+                    .toLocalTime().format(TIME_FORMATTER);
+            framesByDate.computeIfAbsent(date, ignored -> new ArrayList<>()).add(frame);
+        }
+        if (framesByDate.isEmpty()) {
+            return 0;
+        }
+
+        List<AppOrderProInfo> activeOrders = appOrderProInfoMapper.selectList(
+                Wrappers.<AppOrderProInfo>lambdaQuery()
+                        .eq(AppOrderProInfo::getSiteId, siteId)
+                        .eq(AppOrderProInfo::getType, CommonConstant.ORDER_PRO_INFO_TYPE_0)
+                        .in(AppOrderProInfo::getOrderStatus,
+                                CommonConstant.ORDER_STATUS_1, CommonConstant.ORDER_STATUS_2)
+                        .ge(AppOrderProInfo::getUseDateStr, LocalDate.now().format(DATE_FORMATTER))
+                        .in(AppOrderProInfo::getUseDateStr, framesByDate.keySet()));
+
+        Set<String> affectedFamilies = new HashSet<>();
+        int updated = 0;
+        for (AppOrderProInfo order : activeOrders) {
+            List<String> candidates = framesByDate.get(order.getUseDateStr());
+            String newFrame = selectClosestFrame(order.getFrameTimeStr(), candidates);
+            if (StringUtils.isBlank(newFrame)) {
+                continue;
+            }
+            affectedFamilies.add(order.getFamilyUserId());
+            if (!Objects.equals(order.getFrameTimeStr(), newFrame)) {
+                order.setFrameTimeStr(newFrame);
+                order.setExpireTime(order.getUseDateStr() + " " + newFrame.substring(newFrame.indexOf('-') + 1));
+                order.setUpdateTime(new Date());
+                updated += appOrderProInfoMapper.updateById(order);
+            }
+        }
+
+        queueAfterCommit(siteId, affectedFamilies);
+        log.info("学校开放时段变更已同步到存量订单,siteId={}, matchedOrders={}, updatedOrders={}, families={}",
+                siteId, activeOrders.size(), updated, affectedFamilies.size());
+        return updated;
+    }
+
+    private void queueAfterCommit(String siteId, Set<String> familyIds) {
+        if (CollUtil.isEmpty(familyIds)) {
+            return;
+        }
+        Set<String> snapshot = new HashSet<>(familyIds);
+        Runnable task = () -> {
+            for (String familyId : snapshot) {
+                try {
+                    synchronizeFamilySite(familyId, siteId);
+                } catch (Exception e) {
+                    log.error("开放时段变更后门禁权限重推失败,siteId={}, familyId={}", siteId, familyId, e);
+                }
+            }
+        };
+        if (TransactionSynchronizationManager.isSynchronizationActive()
+                && TransactionSynchronizationManager.isActualTransactionActive()) {
+            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+                @Override
+                public void afterCommit() {
+                    ioIntensiveTaskExecutor.execute(task);
+                }
+            });
+        } else {
+            ioIntensiveTaskExecutor.execute(task);
+        }
+    }
+
+    static AccessWindow buildAccessWindow(List<AppOrderProInfo> orders) {
+        if (CollUtil.isEmpty(orders)) {
+            return null;
+        }
+        LocalDate beginDate = null;
+        LocalDate endDate = null;
+        LocalTime beginTime = null;
+        LocalTime endTime = null;
+        for (AppOrderProInfo order : orders) {
+            try {
+                LocalDate date = LocalDate.parse(order.getUseDateStr(), DATE_FORMATTER);
+                String[] times = StringUtils.split(order.getFrameTimeStr(), '-');
+                if (times == null || times.length != 2) {
+                    continue;
+                }
+                LocalTime start = LocalTime.parse(times[0], TIME_FORMATTER);
+                LocalTime end = LocalTime.parse(times[1], TIME_FORMATTER);
+                if (end.isBefore(start)) {
+                    continue;
+                }
+                beginDate = beginDate == null || date.isBefore(beginDate) ? date : beginDate;
+                endDate = endDate == null || date.isAfter(endDate) ? date : endDate;
+                beginTime = beginTime == null || start.isBefore(beginTime) ? start : beginTime;
+                endTime = endTime == null || end.isAfter(endTime) ? end : endTime;
+            } catch (Exception e) {
+                log.warn("忽略无法解析的场地订单门禁时段,orderId={}, useDateStr={}, frameTimeStr={}",
+                        order.getId(), order.getUseDateStr(), order.getFrameTimeStr());
+            }
+        }
+        if (beginDate == null || endDate == null || beginTime == null || endTime == null) {
+            return null;
+        }
+        return new AccessWindow(beginDate, endDate, beginTime, endTime);
+    }
+
+    private static String selectClosestFrame(String currentFrame, List<String> candidates) {
+        if (CollUtil.isEmpty(candidates)) {
+            return null;
+        }
+        if (candidates.contains(currentFrame)) {
+            return currentFrame;
+        }
+        if (candidates.size() == 1) {
+            return candidates.get(0);
+        }
+        long bestDistance = Long.MAX_VALUE;
+        String best = candidates.get(0);
+        LocalTime[] current = parseFrame(currentFrame);
+        if (current == null) {
+            return best;
+        }
+        for (String candidate : candidates) {
+            LocalTime[] parsed = parseFrame(candidate);
+            if (parsed == null) {
+                continue;
+            }
+            long distance = Math.abs(current[0].toSecondOfDay() - parsed[0].toSecondOfDay())
+                    + Math.abs(current[1].toSecondOfDay() - parsed[1].toSecondOfDay());
+            if (distance < bestDistance) {
+                bestDistance = distance;
+                best = candidate;
+            }
+        }
+        return best;
+    }
+
+    private static LocalTime[] parseFrame(String frame) {
+        try {
+            String[] values = StringUtils.split(frame, '-');
+            if (values == null || values.length != 2) {
+                return null;
+            }
+            return new LocalTime[]{LocalTime.parse(values[0], TIME_FORMATTER),
+                    LocalTime.parse(values[1], TIME_FORMATTER)};
+        } catch (Exception ignored) {
+            return null;
+        }
+    }
+
+    private void invokeHikiotWithRetry(String operation, String deviceSerial, Supplier<String> request) {
+        RuntimeException lastException = null;
+        for (int attempt = 1; attempt <= MAX_HIKIOT_ATTEMPTS; attempt++) {
+            try {
+                String response = request.get();
+                JsonObject json = JsonParser.parseString(response).getAsJsonObject();
+                if (json.has("code") && json.get("code").getAsInt() == 0) {
+                    return;
+                }
+                String message = json.has("msg") ? json.get("msg").getAsString() : "未知错误";
+                lastException = new JeecgBootException(operation + "下发失败: " + message);
+            } catch (RuntimeException e) {
+                lastException = e;
+            }
+            if (attempt < MAX_HIKIOT_ATTEMPTS) {
+                try {
+                    Thread.sleep(300L * attempt);
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    throw new JeecgBootException(operation + "下发被中断", e);
+                }
+            }
+        }
+        log.error("海康{}下发失败,deviceSerial={},已重试{}次", operation, deviceSerial, MAX_HIKIOT_ATTEMPTS,
+                lastException);
+        throw new JeecgBootException("设备" + operation + "录入失败,请联系管理员", lastException);
+    }
+
+    @Getter
+    static class AccessWindow {
+        private final LocalDate beginDate;
+        private final LocalDate endDate;
+        private final LocalTime beginTime;
+        private final LocalTime endTime;
+
+        AccessWindow(LocalDate beginDate, LocalDate endDate, LocalTime beginTime, LocalTime endTime) {
+            this.beginDate = beginDate;
+            this.endDate = endDate;
+            this.beginTime = beginTime;
+            this.endTime = endTime;
+        }
+    }
+}

+ 5 - 85
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/app/service/impl/OrderServiceImpl.java

@@ -31,6 +31,7 @@ import org.jeecg.common.util.DictAnnotationUtil;
 import org.jeecg.modules.app.dto.AppOrderInfoDTO;
 import org.jeecg.modules.app.form.*;
 import org.jeecg.modules.app.service.FaceImageNormalizationService;
+import org.jeecg.modules.app.service.FieldAccessPermissionService;
 import org.jeecg.modules.app.service.IOrderService;
 import org.jeecg.modules.app.service.WeChatPayService;
 import org.jeecg.modules.app.vo.AppGameScheduleVO;
@@ -100,6 +101,8 @@ public class OrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> impl
     @Resource
     private FaceImageNormalizationService faceImageNormalizationService;
     @Resource
+    private FieldAccessPermissionService fieldAccessPermissionService;
+    @Resource
     private AppTeachingTimeMapper appTeachingTimeMapper;
     @Resource
     private InsureOrderInfoMapper insureOrderInfoMapper;
@@ -1372,91 +1375,8 @@ public class OrderServiceImpl extends ServiceImpl<AppOrderMapper, AppOrder> impl
                 log.info("订单ID: {}, 用户: {}, useDateStr: {}, frameTimeStr: {}", 
                         appOrderProInfo.getId(), appOrderProInfo.getUserName(), 
                         appOrderProInfo.getUseDateStr(), appOrderProInfo.getFrameTimeStr());
-                
-                FamilyMembers familyMembers = familyMembersMapper.selectById(appOrderProInfo.getFamilyUserId());
-                faceImageNormalizationService.ensureNormalized(familyMembers);
-                // 查询该用户所有待使用的场地订单,找出最早和最晚的使用日期
-                List<AppOrderProInfo> allFieldOrders = appOrderProInfoMapper.selectList(Wrappers.<AppOrderProInfo>lambdaQuery()
-                        .eq(AppOrderProInfo::getFamilyUserId, appOrderProInfo.getFamilyUserId())
-                        .eq(AppOrderProInfo::getOrderStatus, 1)
-                        .eq(AppOrderProInfo::getType, 0)
-                        .isNotNull(AppOrderProInfo::getUseDateStr));
-                
-                log.info("查询到用户 {} 的场地订单数量: {}", familyMembers.getFullName(), allFieldOrders.size());
-                
-                if (CollUtil.isNotEmpty(allFieldOrders)) {
-                    // 解析所有订单的useDateStr,找出最早和最晚的日期
-                    LocalDate earliestDate = null;
-                    LocalDate latestDate = null;
-                    
-                    for (AppOrderProInfo fieldOrder : allFieldOrders) {
-                        String useDateStr = fieldOrder.getUseDateStr();
-                        if (useDateStr != null && !useDateStr.isEmpty()) {
-                            try {
-                                // 解析日期 "yyyy-MM-dd"
-                                LocalDate useDate = LocalDate.parse(useDateStr);
-                                log.info("  订单 {} 的useDateStr: {} -> 解析为: {}", 
-                                        fieldOrder.getId(), useDateStr, useDate);
-                                
-                                // 找出最早的日期
-                                if (earliestDate == null || useDate.isBefore(earliestDate)) {
-                                    earliestDate = useDate;
-                                }
-                                // 找出最晚的日期
-                                if (latestDate == null || useDate.isAfter(latestDate)) {
-                                    latestDate = useDate;
-                                }
-                            } catch (Exception e) {
-                                // 解析失败,跳过该订单
-                                log.error("解析场地订单useDateStr失败: {}", useDateStr, e);
-                            }
-                        }
-                    }
-                    
-                    log.info("日期范围统计 - 最早: {}, 最晚: {}", earliestDate, latestDate);
-                    
-                    // 使用解析到的日期范围更新门禁权限
-                    if (earliestDate != null && latestDate != null) {
-                        List<AppDevice> siteDevices = appDeviceMapper.selectList(Wrappers.<AppDevice>lambdaQuery()
-                                .eq(AppDevice::getSiteId, appOrderProInfo.getSiteId()));
-                        if (CollUtil.isEmpty(siteDevices)) {
-                            log.error("场地订单门禁下发失败:场地未配置设备,orderCode={}, siteId={}, orgCode={}",
-                                    appOrder.getOrderCode(), appOrderProInfo.getSiteId(), appOrder.getOrgCode());
-                            throw new JeecgBootException("场地未配置门禁设备,请联系管理员");
-                        }
-                        for (AppDevice appDevice : siteDevices) {
-                            if (null != appDevice) {
-                                // 将LocalDate转换为Date类型,使用正午12:00避免时区问题
-                                Date startDate = Date.from(earliestDate.atTime(12, 0).atZone(ZoneId.systemDefault()).toInstant());
-                                Date endDate = Date.from(latestDate.atTime(12, 0).atZone(ZoneId.systemDefault()).toInstant());
-                                
-                                log.info("转换为Date - startDate: {}, endDate: {}", startDate, endDate);
-                                log.info("准备调用addUser - deviceSerial: {}, employeeNo: {}, frameTimeStr: {}",
-                                        appDevice.getDeviceSerial(), familyMembers.getId(), appOrderProInfo.getFrameTimeStr());
-                                
-                                String addUser = addUser(
-                                        startDate,  // 使用最早的使用日期
-                                        appDevice.getDeviceSerial(),
-                                        appOrderProInfo.getUserName(),
-                                        familyMembers.getId(),
-                                        endDate,  // 使用最晚的使用日期
-                                        appOrderProInfo.getFrameTimeStr());  // 传入时间段
-                                
-                                log.info("addUser返回结果: {}", addUser);
-                                
-                                String addFace = addFace(appDevice.getDeviceSerial(), familyMembers.getId(),
-                                        familyMembers.getRealNameImg());
-                                
-                                log.info("addFace返回结果: {}", addFace);
-                                
-                                JsonObject addUserJson = JsonParser.parseString(addUser).getAsJsonObject();
-                                JsonObject addFaceJson = JsonParser.parseString(addFace).getAsJsonObject();
-                                if (addUserJson.get("code").getAsInt() != 0 || addFaceJson.get("code").getAsInt() != 0)
-                                    throw new JeecgBootException("设备录入用户信息失败!请联系管理员");
-                            }
-                        }
-                    }
-                }
+                fieldAccessPermissionService.synchronizeFamilySite(
+                        appOrderProInfo.getFamilyUserId(), appOrderProInfo.getSiteId());
                 log.info("========== 场地订单门禁设置结束 ==========");
             }
         }

+ 2 - 1
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/hikiot/HikiotTool.java

@@ -647,7 +647,8 @@ public class HikiotTool {
             queryOpenDoor.put("startDate", DateUtil.format(startOfDay, "yyyy-MM-dd HH:mm:ss"));
         }
 //        queryOpenDoor.put("endDate", endOfDay.format(formatter));
-        queryOpenDoor.put("authResults", new int[] {1});
+        // 同步成功和“权限已到期”记录。失败记录用于排障,但不会核销订单。
+        queryOpenDoor.put("authResults", new int[] {1, 5});
 //        queryOpenDoor.put("employeeNos", new String[] {employeeNos});
 //        queryOpenDoor.put("deviceSerial", "FX0889961");
 //        queryOpenDoor.put("deviceSerials", new String[] {"FX0889961"});

+ 6 - 1
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/quartz/job/OrTeachingJobService.java

@@ -268,6 +268,10 @@ public class OrTeachingJobService {
                             .setCreateTime(new Date())
                             .setUpdateTime(new Date())
                             .setDeviceSerialNo(data.getDeviceSerial()));
+                    // 失败记录只落库用于排障,不能把订单误标为已使用。
+                    if (!Integer.valueOf(1).equals(data.getAuthResult())) {
+                        continue;
+                    }
                     //查询相关日期学校子订单
                     // 定义格式化模板
                     SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
@@ -373,6 +377,7 @@ public class OrTeachingJobService {
     @Data
     private static class DoorRecordDTO {
         private String employeeNo;
+        private Integer authResult;
         private String authResultMsg;
         private String gmtCreate;
         private String picUrl;
@@ -381,4 +386,4 @@ public class OrTeachingJobService {
         private String deviceSerial;
     }
 
-}
+}

+ 9 - 3
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/controller/AppTeachingTimeController.java

@@ -15,6 +15,7 @@ import org.jeecg.common.aspect.annotation.AutoLog;
 import org.jeecg.common.system.base.controller.JeecgController;
 import org.jeecg.common.system.query.QueryGenerator;
 import org.jeecg.common.util.DateUtils;
+import org.jeecg.modules.app.service.FieldAccessPermissionService;
 import org.jeecg.modules.system.app.dto.AppTeachingTimeDTO;
 import org.jeecg.modules.system.app.dto.TeachingDayDTO;
 import org.jeecg.modules.system.app.entity.AppSite;
@@ -32,6 +33,7 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.text.SimpleDateFormat;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 
@@ -52,6 +54,8 @@ public class AppTeachingTimeController extends JeecgController<AppTeachingTime,
     private IAppSitePriceRulesService appSitePriceRulesService;
     @Resource
     private IAppSiteService appSiteService;
+    @Resource
+    private FieldAccessPermissionService fieldAccessPermissionService;
 
 	/**
 	 * 分页列表查询
@@ -123,10 +127,8 @@ public class AppTeachingTimeController extends JeecgController<AppTeachingTime,
         String tenantId = teachingTime.getTenantId();
         Date day = teachingTime.getDay();
         AppSitePriceRules sitePriceRules = appSitePriceRulesService.getOne(Wrappers.<AppSitePriceRules>lambdaQuery().eq(AppSitePriceRules::getTenantId, tenantId).eq(AppSitePriceRules::getDateOfSale, day).last("limit 1"));
+        AppSite site = appSiteService.getOne(Wrappers.<AppSite>lambdaQuery().eq(AppSite::getTenantId, tenantId).last("limit 1"));
         if (sitePriceRules != null) {
-
-            AppSite site = appSiteService.getOne(Wrappers.<AppSite>lambdaQuery().eq(AppSite::getTenantId, tenantId).last("limit 1"));
-
             sitePriceRules.setIsTeaching(appTeachingTime.getIsTeaching());
             if (site != null) {
                 if (appTeachingTime.getIsTeaching().equals(1)){
@@ -151,6 +153,10 @@ public class AppTeachingTimeController extends JeecgController<AppTeachingTime,
             appSitePriceRulesService.updateById(sitePriceRules);
         }
         appTeachingTimeService.updateById(appTeachingTime);
+        if (site != null && sitePriceRules != null) {
+            fieldAccessPermissionService.updateFutureOrderSlotsAndQueue(
+                    site.getId(), Collections.singletonList(sitePriceRules));
+        }
         return Result.OK("编辑成功!");
     }
 

+ 4 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/system/app/service/impl/AppSitePlaceServiceImpl.java

@@ -18,6 +18,7 @@ import org.jeecg.common.exception.JeecgBootException;
 import org.jeecg.common.system.vo.LoginUser;
 import org.jeecg.common.util.DateUtils;
 import org.jeecg.modules.app.form.QueryByOrgCodeForm;
+import org.jeecg.modules.app.service.FieldAccessPermissionService;
 import org.jeecg.modules.quartz.utils.HolidayUtil;
 import org.jeecg.modules.system.app.dto.*;
 import org.jeecg.modules.system.app.entity.*;
@@ -56,6 +57,8 @@ public class AppSitePlaceServiceImpl extends ServiceImpl<AppSitePlaceMapper, App
     private AppCategoryMapper appCategoryMapper;
     @Resource
     private AppSiteRuleTimeMapper appSiteRuleTimeMapper;
+    @Resource
+    private FieldAccessPermissionService fieldAccessPermissionService;
 
     //学校 一对多
 
@@ -881,6 +884,7 @@ public class AppSitePlaceServiceImpl extends ServiceImpl<AppSitePlaceMapper, App
             appSitePriceRulesMapper.insertList(appSitePriceRulesList);
 
             appSiteMapper.updateById(site);
+            fieldAccessPermissionService.updateFutureOrderSlotsAndQueue(site.getId(), appSitePriceRulesList);
         }
         //修改学校场地开放信息
         appSitePlace

+ 48 - 0
national-motion-module-system/national-motion-system-biz/src/test/java/org/jeecg/modules/app/service/FieldAccessPermissionServiceTest.java

@@ -0,0 +1,48 @@
+package org.jeecg.modules.app.service;
+
+import org.jeecg.modules.system.app.entity.AppOrderProInfo;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class FieldAccessPermissionServiceTest {
+
+    @Test
+    void combinesDatesAndTimeBoundsFromAllActiveOrders() {
+        AppOrderProInfo first = order("1", "2026-09-29", "12:00:00-22:00:00");
+        AppOrderProInfo second = order("2", "2026-10-02", "08:00:00-18:30:00");
+
+        FieldAccessPermissionService.AccessWindow window =
+                FieldAccessPermissionService.buildAccessWindow(Arrays.asList(first, second));
+
+        assertNotNull(window);
+        assertEquals(LocalDate.of(2026, 9, 29), window.getBeginDate());
+        assertEquals(LocalDate.of(2026, 10, 2), window.getEndDate());
+        assertEquals(LocalTime.of(8, 0), window.getBeginTime());
+        assertEquals(LocalTime.of(22, 0), window.getEndTime());
+    }
+
+    @Test
+    void ignoresMalformedRowsInsteadOfNarrowingValidPermission() {
+        AppOrderProInfo invalid = order("bad", "not-a-date", "broken");
+        AppOrderProInfo valid = order("ok", "2026-09-30", "08:00:00-22:00:00");
+
+        FieldAccessPermissionService.AccessWindow window =
+                FieldAccessPermissionService.buildAccessWindow(Arrays.asList(invalid, valid));
+
+        assertNotNull(window);
+        assertEquals(LocalDate.of(2026, 9, 30), window.getBeginDate());
+        assertEquals(LocalDate.of(2026, 9, 30), window.getEndDate());
+        assertEquals(LocalTime.of(8, 0), window.getBeginTime());
+        assertEquals(LocalTime.of(22, 0), window.getEndTime());
+    }
+
+    private AppOrderProInfo order(String id, String date, String frame) {
+        return new AppOrderProInfo().setId(id).setUseDateStr(date).setFrameTimeStr(frame);
+    }
+}

+ 1 - 1
pom.xml

@@ -563,4 +563,4 @@
 		</snapshotRepository>
 	</distributionManagement>
 
-</project>
+</project>