liujie
2026-09-11 4d15e6b553c448e2e07527e9c06aa4b5f9b099cf
update
5个文件已添加
34个文件已修改
689 ■■■■■ 已修改文件
doc/sql/20260708_visitor_admin_menu.sql 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/sql/20260817_t_visitor_record_add_middle_platform_id.sql 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/sql/20260818_visitor_version_add_edit_permission.sql 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysVersionUpdateController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BaseDataApiUtil.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/MiddlePlatformDataQuery.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-prod.yml 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/logback.xml 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiVisitorRecordController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/ApiPullSignOutDataService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/SysVisitorRecordService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/VisitorRecordScheduleService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/VisitorRegisterSyncService.java 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/resources/application-prod.yml 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/resources/application-test.yml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/resources/logback.xml 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/MiddlePlatformReadonlyInterceptor.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorRecord.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/api/ApiSysVisitorRecord.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysVersionUpdateEditRequest.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysVersionUpdateMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysVersionUpdateService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVersionUpdateServiceImpl.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysVersionUpdateMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysVisitorRecordMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui-dist-20260904-155317.zip 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui-dist-20260904-161418.zip 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/.env.production 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/api/visitor-admin/version.js 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/layout/components/Navbar.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/mixins/middlePlatformMode.js 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/router/index.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/utils/request.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/visitor-admin/account/admin.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/visitor-admin/system/version.vue 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/vue.config.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/sql/20260708_visitor_admin_menu.sql
@@ -41,6 +41,7 @@
  (3061, '入园承诺修改', 3002, 1, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:protocol:edit', '#', 'admin', NOW(), '访客后台按钮权限'),
  (3062, '版本更新新增', 3003, 1, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:version:add', '#', 'admin', NOW(), '访客后台按钮权限'),
  (3063, '版本更新删除', 3003, 2, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:version:remove', '#', 'admin', NOW(), '访客后台按钮权限'),
  (3098, '版本更新编辑', 3003, 3, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:version:edit', '#', 'admin', NOW(), '访客后台按钮权限'),
  (3064, '访客机新增', 3011, 1, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:machine:add', '#', 'admin', NOW(), '访客后台按钮权限'),
  (3065, '访客机编辑', 3011, 2, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:machine:edit', '#', 'admin', NOW(), '访客后台按钮权限'),
  (3066, '访客机启用禁用', 3011, 3, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:machine:status', '#', 'admin', NOW(), '访客后台按钮权限'),
doc/sql/20260817_t_visitor_record_add_middle_platform_id.sql
New file
@@ -0,0 +1,6 @@
-- 保存事件中心返回的访客登记记录ID,用于签离时更新中台状态
ALTER TABLE `t_visitor_record`
    ADD COLUMN `middle_platform_id` varchar(64) DEFAULT NULL COMMENT '事件中心访客登记记录ID' AFTER `register_no`;
ALTER TABLE `t_visitor_record`
    ADD KEY `idx_visitor_record_middle_platform_id` (`middle_platform_id`);
doc/sql/20260818_visitor_version_add_edit_permission.sql
New file
@@ -0,0 +1,30 @@
-- 访客机版本更新增加编辑权限。
-- 可重复执行:固定菜单 ID 使用 3098,并让已有“版本更新新增”权限的角色同步获得编辑权限。
INSERT INTO `sys_menu` (
  `menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`,
  `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`,
  `create_by`, `create_time`, `remark`
) VALUES (
  3098, CONVERT(0xE78988E69CACE69BB4E696B0E7BC96E8BE91 USING utf8mb4), 3003, 3, '#', NULL, NULL,
  '1', '1', 'F', '0', '0', 'visitor:version:edit', '#',
  'admin', NOW(), CONVERT(0xE8AEBFE5AEA2E5908EE58FB0E68C89E992AEE69D83E99990 USING utf8mb4)
)
ON DUPLICATE KEY UPDATE
  `menu_name` = VALUES(`menu_name`),
  `parent_id` = VALUES(`parent_id`),
  `order_num` = VALUES(`order_num`),
  `perms` = VALUES(`perms`),
  `status` = VALUES(`status`),
  `update_by` = 'admin',
  `update_time` = NOW();
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT source_role.`role_id`, 3098
FROM `sys_role_menu` source_role
WHERE source_role.`menu_id` = 3062
  AND NOT EXISTS (
    SELECT 1
    FROM `sys_role_menu` target_role
    WHERE target_role.`role_id` = source_role.`role_id`
      AND target_role.`menu_id` = 3098
  );
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysVersionUpdateController.java
@@ -11,6 +11,7 @@
import com.ruoyi.system.dto.SysVersionUpdateAddRequest;
import com.ruoyi.system.dto.SysVersionUpdateDeleteRequest;
import com.ruoyi.system.dto.SysVersionUpdateDetailRequest;
import com.ruoyi.system.dto.SysVersionUpdateEditRequest;
import com.ruoyi.system.dto.SysVersionUpdatePageRequest;
import com.ruoyi.system.service.ISysVersionUpdateService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -73,8 +74,19 @@
        return toAjax(versionUpdateService.add(request, getUsername()));
    }
    /**
     * 编辑版本更新
     */
    @Log(title = "版本更新", businessType = BusinessType.UPDATE)
    @PreAuthorize("@ss.hasPermi('visitor:version:edit')")
    @PostMapping("/edit")
    public AjaxResult edit(@RequestBody SysVersionUpdateEditRequest request)
    {
        return toAjax(versionUpdateService.edit(request, getUsername()));
    }
    @Log(title = "版本更新", businessType = BusinessType.INSERT)
    @PreAuthorize("@ss.hasPermi('visitor:version:add')")
    @PreAuthorize("@ss.hasAnyPermi('visitor:version:add,visitor:version:edit')")
    @PostMapping("/upload")
    public AjaxResult upload(MultipartFile file)
    {
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java
@@ -180,8 +180,13 @@
        {
            return R.fail("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
        }
        String password = user.getPassword();
        if (StringUtils.isEmpty(password))
        {
            return R.fail("新增用户'" + user.getUserName() + "'失败,用户密码不能为空");
        }
        user.setCreateBy(getUsername());
        user.setPassword(SecurityUtils.encryptPassword("123456"));
        user.setPassword(SecurityUtils.encryptPassword(password));
        user.setRoleType(Integer.parseInt(user.getRoleId().toString()));
        userService.insertUser(user);
        return R.ok();
ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/BaseDataApiUtil.java
@@ -34,7 +34,7 @@
 */
public final class BaseDataApiUtil {
    public static final String DEFAULT_BASE_URL = "http://58.20.112.148:18888";
    public static final String DEFAULT_BASE_URL = "http://192.168.1.146:18888";
    private static final String BASE_URL_PROPERTY = "base.data.api.url";
    private static final String BASE_URL_ENV = "BASE_DATA_API_URL";
    private static final String AUTHORIZATION_HEADER = "Authorization";
ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/MiddlePlatformDataQuery.java
@@ -23,7 +23,7 @@
 *
 * <p>目的:前端页面已绑定本系统字段名,直接透传中台字段会导致表格空列。</p>
 * <p>影响范围:部门、区域、员工、访客机、门禁设备的查询和详情接口。</p>
 * <p>注意:访客记录、入园承诺、版本更新暂无明确基础信息中台接口映射,仍由只读拦截保证不能写。</p>
 * <p>注意:访客记录暂无明确中台接口映射,继续保持只读;入园承诺和版本更新属于本地维护模块。</p>
 */
public final class MiddlePlatformDataQuery {
ruoyi-admin/src/main/resources/application-prod.yml
@@ -15,16 +15,16 @@
  # 验证码类型 math 数字计算 char 字符验证
  captchaType: math
  # 部署模式:standalone单机版 cluster集群版
  deploymentMode: standalone
  deploymentMode: cluster
  # 集群版是否对接中台:true对接 false不对接
  middlePlatformEnabled: false
# 开发环境配置
server:
  # 服务器的HTTP端口,默认为8080
  port: 8081
  port: 9098
  servlet:
    # 应用的访问路径
    context-path: /laboratory/
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
@@ -73,20 +73,13 @@
  # redis 配置
  redis:
    # 地址
    #    host: 127.0.0.1
    #    # 端口,默认为6379
    #    port: 6379
    #    # 数据库索引
    #    database: 0
    #    # 密码
    #    password: 123456
    host: 127.0.0.1
    # 端口,默认为6379
    port: 6379
    port: 26379
    # 数据库索引
    database: 0
    # 密码
    password: 123456
    password: yuyue520
    # 连接超时时间
    timeout: 10s
    lettuce:
@@ -106,9 +99,9 @@
    druid:
      # 主库数据源
      master:
        url: jdbc:mysql://127.0.0.1:10633/laboratory?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
        url: jdbc:mysql://127.0.0.1:23306/fangkeji?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: Laboratory@2025!
        password: yuyue520
      # 从库数据源
      slave:
        # 从数据源开关/默认关闭
@@ -201,7 +194,7 @@
    location: /file/
    qrLocation: /file/qrCode/
    accessPath: /file/
    allowExt: .jpg|.png|.gif|.jpeg|.doc|.docx|.apk|.MP4|.mp4|.pdf|.PDF|.xls|.xlsx|.rar|.zip|.rar|.ppt|.PPT|.pptx|.CAJ|.caj|.bmp
    allowExt: .jpg|.png|.gif|.jpeg|.doc|.docx|.apk|.MP4|.mp4|.pdf|.PDF|.xls|.xlsx|.rar|.zip|.rar
wx:
  conf:
    appId:
ruoyi-admin/src/main/resources/application.yml
@@ -1,4 +1,4 @@
# 项目相关配置
spring:
  profiles:
    active: test
    active: prod
ruoyi-admin/src/main/resources/logback.xml
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 日志存放路径 -->
    <property name="log.path" value="${LOG_PATH:-/home/ruoyi/logs}" />
    <!-- 日志输出格式 -->
    <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
@@ -12,65 +10,6 @@
        </encoder>
    </appender>
    
    <!-- 系统日志输出 -->
    <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-info.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
            <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>INFO</level>
            <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
            <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-error.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>ERROR</level>
            <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
            <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- 用户访问日志输出  -->
    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-user.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 按天回滚 daily -->
            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
    </appender>
    <!-- 系统模块日志级别控制  -->
    <logger name="com.ruoyi" level="info" />
    <!-- Spring日志级别控制  -->
@@ -79,15 +18,4 @@
    <root level="info">
        <appender-ref ref="console" />
    </root>
    <!--系统操作日志-->
    <root level="info">
        <appender-ref ref="file_info" />
        <appender-ref ref="file_error" />
    </root>
    <!--系统用户操作日志-->
    <logger name="sys-user" level="info">
        <appender-ref ref="sys-user"/>
    </logger>
</configuration>
</configuration>
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiVisitorRecordController.java
@@ -245,8 +245,10 @@
            if (syncResult.isSuccess()) {
                logger.info("访客登记数据已同步到对端,远程ID:{}", syncResult.get("data"));
                // 可将远程ID保存到本地记录
//                record.setRemoteId(syncResult.getRemoteId());
                Object middlePlatformId = syncResult.get("data");
                if (middlePlatformId != null && StringUtils.isNotBlank(String.valueOf(middlePlatformId))) {
                    record.setMiddlePlatformId(String.valueOf(middlePlatformId));
                }
                UpdateWrapper<SysVisitorRecord> wrapper = new UpdateWrapper<>();
                wrapper.eq("record_id", record.getRecordId());  // 指定ID条件
                visitorRecordMapper.update(record, wrapper);
@@ -318,12 +320,15 @@
        visitorRecord.setSignVerifyMethod(request.getSignVerifyMethod());
        visitorRecord.setStatus(record.getStatus());
        visitorRecord.setRecordId(record.getRecordId());
        visitorRecord.setSignTime(new Date());
        Date signOutTime = new Date();
        visitorRecord.setSignTime(signOutTime);
        record.setSignTime(signOutTime);
        UpdateWrapper<SysVisitorRecord> wrapperUpdate = new UpdateWrapper<>();
        wrapperUpdate.eq("record_id", visitorRecord.getRecordId());  // 指定ID条件
        sysVisitorRecordService.revokeAccess(record);
        visitorRecordMapper.update(visitorRecord, wrapperUpdate);
        sysVisitorRecordService.syncMiddlePlatformStatus(record);
        return success();
    }
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/ApiPullSignOutDataService.java
@@ -267,6 +267,13 @@
            return false;
        }
        record.setSignTime(signoutTemp.getSignTime());
        record.setSignReceiverName(signoutTemp.getSignReceiverName());
        record.setSignVerifyMethod(signoutTemp.getSignVerifyMethod());
        record.setSignImageUrl(signoutTemp.getSignImageUrl());
        record.setStatus(status);
        visitorRecordService.syncMiddlePlatformStatus(record);
        // 4. 更新签离临时表关联状态
        signoutTemp.setRelateStatus("1");
        signoutTemp.setRelateRecordId(record.getRecordId());
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/SysVisitorRecordService.java
@@ -29,6 +29,9 @@
    @Autowired
    private DahuaVisitorAccessService dahuaVisitorAccessService;
    @Autowired
    private VisitorRegisterSyncService visitorRegisterSyncService;
    private static final String REDIS_KEY_PREFIX = "visitor:expire:";
    /**
@@ -136,6 +139,7 @@
        record.setRemark("逾期滞留,等待确认");
        record.setUpdateTime(now);
        this.updateById(record);
        syncMiddlePlatformStatus(record);
    }
    /**
@@ -143,6 +147,13 @@
     */
    public void revokeAccess(SysVisitorRecord record) {
        dahuaVisitorAccessService.revoke(record);
    }
    /**
     * 同步访客状态到事件中心;失败不回滚本地签离结果。
     */
    public void syncMiddlePlatformStatus(SysVisitorRecord record) {
        visitorRegisterSyncService.syncVisitorStatus(record);
    }
    private void registerGrantRollbackCompensation(SysVisitorRecord record,
@@ -189,6 +200,8 @@
            redisTemplate.delete(REDIS_KEY_PREFIX + record.getRecordId());
        }
        syncMiddlePlatformStatus(record);
        return true;
    }
@@ -221,6 +234,7 @@
                record.setRemark("系统自动签离(启动时检查)");
                record.setUpdateTime(now);
                this.updateById(record);
                syncMiddlePlatformStatus(record);
                // 删除 Redis Key
                redisTemplate.delete(REDIS_KEY_PREFIX + record.getRecordId());
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/VisitorRecordScheduleService.java
@@ -88,10 +88,11 @@
                    }
                    // 更新记录状态为超期自动签离
                    Date signOutTime = new Date();
                    LambdaUpdateWrapper<SysVisitorRecord> updateWrapper = new LambdaUpdateWrapper<>();
                    updateWrapper.eq(SysVisitorRecord::getRecordId, record.getRecordId())
                            .set(SysVisitorRecord::getStatus, "OVERDUE_AUTO_SIGNED_OUT")
                            .set(SysVisitorRecord::getSignTime, new Date()) // 签离时间设为有效期结束时间
                            .set(SysVisitorRecord::getSignTime, signOutTime) // 签离时间设为当前时间
                            .set(SysVisitorRecord::getSignReceiverName, "系统自动")
                            .set(SysVisitorRecord::getSignVerifyMethod, "AUTO")
                            .set(SysVisitorRecord::getSignImageUrl, null)
@@ -104,6 +105,9 @@
                    int updateCount = visitorRecordMapper.update(null, updateWrapper);
                    if (updateCount > 0) {
                        record.setStatus("OVERDUE_AUTO_SIGNED_OUT");
                        record.setSignTime(signOutTime);
                        visitorRecordService.syncMiddlePlatformStatus(record);
                        successCount++;
                        log.info("超期自动签离成功,recordId: {}, 姓名: {}, 身份证号: {}, 有效期结束时间: {}",
                                record.getRecordId(),
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/VisitorRegisterSyncService.java
@@ -19,6 +19,7 @@
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
@@ -32,8 +33,12 @@
    @Value("${visitor.sync.url:}")
    private String syncUrl;
    @Value("${visitor.sync.enabled:}")
    @Value("${visitor.sync.enabled:false}")
    private Boolean syncEnabled;
    /** 中台访客状态更新地址;未配置时由登记地址自动推导。 */
    @Value("${visitor.sync.status-url:}")
    private String statusUrl;
    private final ObjectMapper objectMapper = new ObjectMapper();
@@ -48,7 +53,7 @@
     */
    public AjaxResult syncVisitorRegister(SysVisitorRecord record) {
        // 检查同步开关
        if (!syncEnabled) {
        if (!Boolean.TRUE.equals(syncEnabled)) {
            log.info("访客登记同步已关闭,跳过同步");
            return AjaxResult.success("同步功能未开启");
        }
@@ -80,8 +85,15 @@
            boolean success = jsonNode.has("success") && jsonNode.get("success").asBoolean();
            if (success) {
                String remoteId = jsonNode.has("data") && jsonNode.get("data").has("id")
                        ? jsonNode.get("data").get("id").asText() : null;
                JsonNode dataNode = jsonNode.get("data");
                String remoteId = null;
                if (dataNode != null) {
                    if (dataNode.isObject() && dataNode.has("id")) {
                        remoteId = dataNode.get("id").asText();
                    } else if (dataNode.isValueNode()) {
                        remoteId = dataNode.asText();
                    }
                }
                log.info("访客登记同步成功,远程ID:{}", remoteId);
                return AjaxResult.success(remoteId);
            } else {
@@ -93,6 +105,70 @@
        } catch (Exception e) {
            log.error("访客登记同步异常", e);
            return AjaxResult.error("同步异常:" + e.getMessage());
        }
    }
    /**
     * 将本地签离状态同步到事件中心。
     *
     * <p>状态同步采用尽力而为策略:中台不可用时保留本地签离结果,避免访客机因网络问题无法完成签离。</p>
     */
    public AjaxResult syncVisitorStatus(SysVisitorRecord record) {
        if (!Boolean.TRUE.equals(syncEnabled)) {
            return AjaxResult.success("访客状态同步已关闭");
        }
        if (record == null || StringUtils.isBlank(record.getMiddlePlatformId())) {
            log.warn("访客状态同步跳过:缺少中台访客登记记录ID,本地recordId={}",
                    record == null ? null : record.getRecordId());
            return AjaxResult.warn("缺少中台访客登记记录ID");
        }
        String middleStatus = toMiddleStatus(record.getStatus());
        if (middleStatus == null) {
            log.info("访客状态 {} 无需同步到中台,middlePlatformId={}",
                    record.getStatus(), record.getMiddlePlatformId());
            return AjaxResult.success("当前状态无需同步");
        }
        String targetUrl = resolveStatusUrl();
        if (StringUtils.isBlank(targetUrl)) {
            log.warn("访客状态同步跳过:未配置中台状态更新地址");
            return AjaxResult.warn("中台状态更新地址未配置");
        }
        try {
            Map<String, Object> requestBody = new LinkedHashMap<>();
            requestBody.put("id", record.getMiddlePlatformId());
            requestBody.put("status", middleStatus);
            if (record.getSignTime() != null) {
                requestBody.put("signOutTime", formatTime(record.getSignTime()));
            }
            String jsonBody = objectMapper.writeValueAsString(requestBody);
            log.info("同步访客状态到中台:middlePlatformId={}, status={}, signOutTime={}",
                    record.getMiddlePlatformId(), middleStatus, requestBody.get("signOutTime"));
            HttpResponse response = HttpRequest.post(targetUrl)
                    .header("Content-Type", ContentType.JSON.getValue())
                    .body(jsonBody)
                    .timeout(30000)
                    .execute();
            String responseBody = response.body();
            JsonNode jsonNode = objectMapper.readTree(responseBody);
            boolean success = jsonNode.has("success") && jsonNode.get("success").asBoolean();
            if (success) {
                return AjaxResult.success("访客状态同步成功");
            }
            String errorMessage = jsonNode.has("description")
                    ? jsonNode.get("description").asText() : "中台访客状态更新失败";
            log.error("中台访客状态更新失败,middlePlatformId={}, status={}, message={}",
                    record.getMiddlePlatformId(), middleStatus, errorMessage);
            return AjaxResult.error(errorMessage);
        } catch (Exception e) {
            log.error("中台访客状态同步异常,middlePlatformId={}, status={}",
                    record.getMiddlePlatformId(), middleStatus, e);
            return AjaxResult.error("中台状态同步异常:" + e.getMessage());
        }
    }
@@ -133,6 +209,35 @@
         params.put("pic", record.getFaceImageUrl());
        return params;
    }
    /** 根据登记接口地址推导同一事件中心下的状态更新接口。 */
    private String resolveStatusUrl() {
        if (StringUtils.isNotBlank(statusUrl)) {
            return statusUrl.trim();
        }
        if (StringUtils.isBlank(syncUrl)) {
            return null;
        }
        String registerPath = "/visitor-register";
        if (syncUrl.contains(registerPath)) {
            return syncUrl.replace(registerPath, "/visitor-status-update");
        }
        return null;
    }
    /** 将本地状态转换为事件中心字典值。 */
    private String toMiddleStatus(String status) {
        if ("SIGNED_OUT".equals(status)) {
            return "6";
        }
        if ("OVERDUE_AUTO_SIGNED_OUT".equals(status)) {
            return "5";
        }
        if ("PENDING_CONFIRM".equals(status)) {
            return "7";
        }
        return null;
    }
    /**
@@ -188,4 +293,4 @@
        String registerNo = record.getRegisterNo();
        return String.format("vms:visitor:%s:%s", dateStr, registerNo);
    }
}
}
ruoyi-applet/src/main/resources/application-prod.yml
@@ -14,11 +14,10 @@
  addressEnabled: false
  # 验证码类型 math 数字计算 char 字符验证
  captchaType: math
# 开发环境配置
server:
  # 服务器的HTTP端口,默认为8080
  port: 8082
  port: 9099
  servlet:
    # 应用的访问路径
    context-path: /
@@ -32,6 +31,14 @@
      max: 800
      # Tomcat启动初始化的线程数,默认值10
      min-spare: 100
# 访客登记同步配置
visitor:
  sync:
    enabled: true  # 是否启用同步
    url: http://192.168.1.146:18888/event-center/security-protection/visitor-register
    # 状态更新地址;留空时按 url 自动替换为 visitor-status-update
    status-url: ${VISITOR_SYNC_STATUS_URL:}
# 日志配置
logging:
@@ -70,20 +77,13 @@
  # redis 配置
  redis:
    # 地址
    #    host: 127.0.0.1
    #    # 端口,默认为6379
    #    port: 6379
    #    # 数据库索引
    #    database: 0
    #    # 密码
    #    password: 123456
    host: 127.0.0.1
    # 端口,默认为6379
    port: 16379
    port: 26379
    # 数据库索引
    database: 0
    # 密码
    password: 8f5z9g52gx4bg
    password: yuyue520
    # 连接超时时间
    timeout: 10s
    lettuce:
@@ -103,9 +103,9 @@
    druid:
      # 主库数据源
      master:
        url: jdbc:mysql://172.27.0.13:3306/xizang?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai
        username: xzgt
        password: changyun!6f2gshj6h3j
        url: jdbc:mysql://127.0.0.1:23306/fangkeji?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: yuyue520
      # 从库数据源
      slave:
        # 从数据源开关/默认关闭
@@ -198,33 +198,8 @@
    location: /file/
    qrLocation: /file/qrCode/
    accessPath: /file/
    allowExt: .jpg|.png|.gif|.jpeg|.doc|.docx|.apk|.MP4|.mp4|.pdf|.PDF
  url:
    prefix: https://xzgt.test.591taxi.cn:${server.port}${server.servlet.context-path}
    allowExt: .jpg|.png|.gif|.jpeg|.doc|.docx|.apk|.MP4|.mp4|.pdf|.PDF|.xls|.xlsx|.rar|.zip|.rar|.m4a
wx:
  conf:
    appId: wxe91f1af7638aa5dd
    secretId: a787e1a462715604e0c9528b6d8960d1
#OSS及短信配置
code:
  config:
    templateCodeTest: "SMS_154950909"
    signNameTest: "阿里云短信测试"
    accessKeyId: LTAI5tAdba8HtT1C6UqtSxBt
    accessKeySecret: 0SRb6XGkciQDPWn2rYqbJtq2qRMDY8
    signName: "四川金达通信工程"
    templateCode: "SMS_293985284"
cos:
  client:
    accessKey: AKIDCF5EF2c0DE1e5JK8r4EGJF4mNsMgp26x
    secretKey: lLl184rUyFOOE0d5KNGC3kmfNsCWk4GU
    bucket: xzgttest-1305134071
    bucketAddr: ap-chengdu
    rootSrc: https://xzgttest-1305134071.cos.ap-chengdu.myqcloud.com/
    location: xizang
sms:
  enable: true
  appId: 1400957506
  secretid: AKIDCF5EF2c0DE1e5JK8r4EGJF4mNsMgp26x
  secretkey: lLl184rUyFOOE0d5KNGC3kmfNsCWk4GU
  sign: 畅云出行
    appId:
    secretId:
ruoyi-applet/src/main/resources/application-test.yml
@@ -36,7 +36,9 @@
visitor:
  sync:
    enabled: true  # 是否启用同步
    url: http://58.20.112.148:18888/event-center/security-protection/visitor-register
    url: http://192.168.1.146:18888/event-center/security-protection/visitor-register
    # 状态更新地址;留空时按 url 自动替换为 visitor-status-update
    status-url: ${VISITOR_SYNC_STATUS_URL:}
# 日志配置
logging:
@@ -200,4 +202,4 @@
wx:
  conf:
    appId:
    secretId:
    secretId:
ruoyi-applet/src/main/resources/application.yml
@@ -1,7 +1,7 @@
# 项目相关配置
spring:
  profiles:
    active: test
    active: prod
# 大华门禁访客人脸授权配置。默认关闭;原生库请使用对应操作系统的官方 SDK 目录。
dahua:
ruoyi-applet/src/main/resources/logback.xml
@@ -1,7 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 日志存放路径 -->
    <property name="log.path" value="/home/ruoyi/logs" />
    <!-- 日志输出格式 -->
    <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
@@ -12,65 +10,6 @@
        </encoder>
    </appender>
    
    <!-- 系统日志输出 -->
    <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-info.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
            <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>INFO</level>
            <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
            <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-error.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>ERROR</level>
            <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
            <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <!-- 用户访问日志输出  -->
    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-user.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 按天回滚 daily -->
            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
    </appender>
    <!-- 系统模块日志级别控制  -->
    <logger name="com.ruoyi" level="info" />
    <!-- Spring日志级别控制  -->
@@ -79,15 +18,4 @@
    <root level="info">
        <appender-ref ref="console" />
    </root>
    <!--系统操作日志-->
    <root level="info">
        <appender-ref ref="file_info" />
        <appender-ref ref="file_error" />
    </root>
    <!--系统用户操作日志-->
    <logger name="sys-user" level="info">
        <appender-ref ref="sys-user"/>
    </logger>
</configuration>
</configuration>
ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/MiddlePlatformReadonlyInterceptor.java
@@ -37,11 +37,13 @@
            "/system/role/manage"
    );
    /** 本地仍可维护的白名单模块:账号管理、系统配置、首页、操作日志。 */
    /** 本地仍可维护的白名单模块:账号管理、系统配置、入园承诺、访客机版本、首页、操作日志。 */
    private static final List<String> LOCAL_MODULE_PREFIXES = Arrays.asList(
            "/system/platform/admin",
            "/system/role/manage",
            "/system/visitor/config",
            "/system/protocol/config",
            "/system/version/update",
            "/system/dashboard",
            "/system/operation/log"
    );
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorRecord.java
@@ -28,6 +28,9 @@
    /** 登记编号 */
    private String registerNo;
    /** 中台访客登记记录ID,登记同步成功后保存 */
    private String middlePlatformId;
    /** 访客姓名 */
    private String visitorName;
ruoyi-system/src/main/java/com/ruoyi/system/domain/api/ApiSysVisitorRecord.java
@@ -27,6 +27,9 @@
    /** 登记编号 */
    private String registerNo;
    /** 中台访客登记记录ID,登记同步成功后保存 */
    private String middlePlatformId;
    /** 访客姓名 */
    private String visitorName;
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysVersionUpdateEditRequest.java
New file
@@ -0,0 +1,37 @@
package com.ruoyi.system.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * 版本更新编辑请求
 *
 * @author ruoyi
 */
@Data
public class SysVersionUpdateEditRequest
{
    /** 更新主键 */
    @ApiModelProperty("更新主键")
    private Long updateId;
    /** 版本号 */
    @ApiModelProperty("版本号")
    private String versionNo;
    /** 更新内容 */
    @ApiModelProperty("更新内容")
    private String updateContent;
    /** 安装包地址 */
    @ApiModelProperty("安装包地址")
    private String packageUrl;
    /** 安装包名称 */
    @ApiModelProperty("安装包名称")
    private String packageName;
    /** 是否强制更新 */
    @ApiModelProperty("是否强制更新")
    private Boolean forceUpdate;
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysVersionUpdateMapper.java
@@ -26,5 +26,7 @@
    public int insertVersionUpdate(SysVersionUpdate versionUpdate);
    public int updateVersionUpdate(SysVersionUpdate versionUpdate);
    public int deleteByIds(@Param("updateIds") List<Long> updateIds);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysVersionUpdateService.java
@@ -4,6 +4,7 @@
import com.ruoyi.system.dto.SysVersionUpdateAddRequest;
import com.ruoyi.system.dto.SysVersionUpdateDeleteRequest;
import com.ruoyi.system.dto.SysVersionUpdateDetailRequest;
import com.ruoyi.system.dto.SysVersionUpdateEditRequest;
import com.ruoyi.system.dto.SysVersionUpdatePageRequest;
import com.ruoyi.system.vo.SysVersionUpdateVO;
@@ -22,5 +23,7 @@
    public int add(SysVersionUpdateAddRequest request, String username);
    public int edit(SysVersionUpdateEditRequest request, String username);
    public int delete(SysVersionUpdateDeleteRequest request);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVersionUpdateServiceImpl.java
@@ -7,6 +7,7 @@
import com.ruoyi.system.dto.SysVersionUpdateAddRequest;
import com.ruoyi.system.dto.SysVersionUpdateDeleteRequest;
import com.ruoyi.system.dto.SysVersionUpdateDetailRequest;
import com.ruoyi.system.dto.SysVersionUpdateEditRequest;
import com.ruoyi.system.dto.SysVersionUpdatePageRequest;
import com.ruoyi.system.mapper.SysVersionUpdateMapper;
import com.ruoyi.system.service.ISysVersionUpdateService;
@@ -80,7 +81,35 @@
        {
            throw new ServiceException("版本更新内容不能为空");
        }
        String versionNo = StringUtils.trim(request.getVersionNo());
        SysVersionUpdate versionUpdate = buildVersionUpdate(request.getVersionNo(), request.getUpdateContent(),
                request.getPackageUrl(), request.getPackageName(), request.getForceUpdate());
        versionUpdate.setCreateBy(username);
        return versionUpdateMapper.insertVersionUpdate(versionUpdate);
    }
    @Override
    public int edit(SysVersionUpdateEditRequest request, String username)
    {
        if (request == null || request.getUpdateId() == null || request.getUpdateId() <= 0)
        {
            throw new ServiceException("更新主键不能为空");
        }
        if (versionUpdateMapper.selectById(request.getUpdateId()) == null)
        {
            throw new ServiceException("版本更新记录不存在");
        }
        SysVersionUpdate versionUpdate = buildVersionUpdate(request.getVersionNo(), request.getUpdateContent(),
                request.getPackageUrl(), request.getPackageName(), request.getForceUpdate());
        versionUpdate.setUpdateId(request.getUpdateId());
        versionUpdate.setUpdateBy(username);
        return versionUpdateMapper.updateVersionUpdate(versionUpdate);
    }
    private SysVersionUpdate buildVersionUpdate(String requestVersionNo, String requestUpdateContent,
                                                 String requestPackageUrl, String requestPackageName,
                                                 Boolean requestForceUpdate)
    {
        String versionNo = StringUtils.trim(requestVersionNo);
        if (StringUtils.isEmpty(versionNo))
        {
            throw new ServiceException("版本号不能为空");
@@ -91,21 +120,20 @@
            throw new ServiceException("版本号格式不正确,请输入数字或使用小数点分隔");
        }
        versionNo = versionNumber;
        String updateContent = StringUtils.trim(request.getUpdateContent());
        String packageUrl = StringUtils.trim(request.getPackageUrl());
        String updateContent = StringUtils.trim(requestUpdateContent);
        String packageUrl = StringUtils.trim(requestPackageUrl);
        if (StringUtils.isEmpty(packageUrl))
        {
            throw new ServiceException("安装包不能为空");
        }
        String packageName = StringUtils.trim(request.getPackageName());
        String packageName = StringUtils.trim(requestPackageName);
        SysVersionUpdate versionUpdate = new SysVersionUpdate();
        versionUpdate.setVersionNo(versionNo);
        versionUpdate.setUpdateContent(updateContent);
        versionUpdate.setPackageUrl(packageUrl);
        versionUpdate.setPackageName(StringUtils.isEmpty(packageName) ? packageUrl : packageName);
        versionUpdate.setForceUpdate(request.getForceUpdate() == null ? Boolean.FALSE : request.getForceUpdate());
        versionUpdate.setCreateBy(username);
        return versionUpdateMapper.insertVersionUpdate(versionUpdate);
        versionUpdate.setForceUpdate(requestForceUpdate == null ? Boolean.FALSE : requestForceUpdate);
        return versionUpdate;
    }
    @Override
ruoyi-system/src/main/resources/mapper/system/SysVersionUpdateMapper.xml
@@ -69,6 +69,19 @@
        )
    </insert>
    <update id="updateVersionUpdate" parameterType="SysVersionUpdate">
        update t_version_update
        set version_no = #{versionNo},
            update_content = #{updateContent},
            package_url = #{packageUrl},
            package_name = #{packageName},
            force_update = #{forceUpdate},
            update_by = #{updateBy},
            update_time = sysdate()
        where update_id = #{updateId}
          and del_flag = '0'
    </update>
    <update id="deleteByIds">
        update t_version_update
        set del_flag = '1',
ruoyi-system/src/main/resources/mapper/system/SysVisitorRecordMapper.xml
@@ -222,6 +222,7 @@
            keyProperty="recordId">
        insert into t_visitor_record
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="middlePlatformId != null">middle_platform_id,</if>
            <if test="registerNo != null">register_no,</if>
            <if test="visitorName != null">visitor_name,</if>
            <if test="idCard != null">id_card,</if>
@@ -242,6 +243,7 @@
        </trim>
        values
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="middlePlatformId != null">#{middlePlatformId},</if>
            <if test="registerNo != null">#{registerNo},</if>
            <if test="visitorName != null">#{visitorName},</if>
            <if test="idCard != null">#{idCard},</if>
ruoyi-ui-dist-20260904-155317.zip
Binary files differ
ruoyi-ui-dist-20260904-161418.zip
Binary files differ
ruoyi-ui/.env.production
@@ -7,7 +7,10 @@
# 若依管理系统/生产环境
VUE_APP_BASE_API = '/prod-api'
# 前端静态资源使用相对路径,根目录和子目录部署均可用
VUE_APP_PUBLIC_PATH = './'
VUE_APP_VISITOR_FRONTEND_MODE = 'cluster'
VUE_APP_VISITOR_MIDDLE_PLATFORM_ENABLED = 'true'
VUE_APP_VISITOR_MIDDLE_PLATFORM_ENABLED = 'false'
# VUE_APP_VISITOR_LOGIN_MIDDLE_PLATFORM_ENABLED 未配置时默认跟随 VUE_APP_VISITOR_MIDDLE_PLATFORM_ENABLED
VUE_APP_VISITOR_API_BASE_URL = '/prod-api'
ruoyi-ui/src/api/visitor-admin/version.js
@@ -12,6 +12,10 @@
  return request({ url: '/system/version/update/add', method: 'post', data })
}
export function editVersion(data) {
  return request({ url: '/system/version/update/edit', method: 'post', data })
}
export function uploadVersionPackage(data) {
  return request({
    url: '/system/version/update/upload',
ruoyi-ui/src/layout/components/Navbar.vue
@@ -260,7 +260,7 @@
          this.passwordOpen = false
          setTimeout(() => {
            this.$store.dispatch('LogOut').then(() => {
              location.href = '/login'
              this.$router.push('/login')
            })
          }, 600)
        }).finally(() => {
@@ -280,7 +280,7 @@
        type: 'warning'
      }).then(() => {
        this.$store.dispatch('LogOut').then(() => {
          location.href = '/login'
          this.$router.push('/login')
        })
      }).catch(() => {})
    }
ruoyi-ui/src/mixins/middlePlatformMode.js
@@ -1,9 +1,11 @@
// 集群版对接中台时仍允许本地维护的页面:账号管理、系统配置、首页、操作日志。
// 集群版对接中台时仍允许本地维护的页面:账号管理、系统配置、入园承诺、访客机版本、首页、操作日志。
const localModulePaths = [
  '/index',
  '/visitor/dashboard',
  '/visitor/account',
  '/visitor/system/config',
  '/visitor/system/protocol',
  '/visitor/system/version',
  '/visitor/record/operation-log'
]
ruoyi-ui/src/router/index.js
@@ -344,7 +344,8 @@
}
export default new Router({
  mode: 'history', // 去掉url中的#
  // hash 模式无需服务器为每个前端路由配置 index.html 回退,适合静态目录部署。
  mode: 'hash',
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})
ruoyi-ui/src/utils/request.js
@@ -120,7 +120,7 @@
        MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
          isRelogin.show = false;
          store.dispatch('LogOut').then(() => {
            location.href = '/index';
            location.hash = '#/index';
          })
      }).catch(() => {
        isRelogin.show = false;
ruoyi-ui/src/views/visitor-admin/account/admin.vue
@@ -121,6 +121,14 @@
  name: 'VisitorPlatformAdmin',
  // 目的:初始化管理员列表、查询条件、编辑表单和重置密码表单状态。
  data() {
    const strongPasswordMessage = '密码至少12位,且包含大小写字母和特殊字符'
    const validateStrongPassword = (rule, value, callback) => {
      if (!value || (value.length >= 12 && /[A-Z]/.test(value) && /[a-z]/.test(value) && /[^A-Za-z0-9]/.test(value))) {
        callback()
        return
      }
      callback(new Error(strongPasswordMessage))
    }
    return {
      loading: false,
      showSearch: true,
@@ -148,10 +156,16 @@
        userName: [{ required: true, message: '请输入账号', trigger: 'blur' }],
        phonenumber: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
        roleId: [{ required: true, message: '请选择角色', trigger: 'change' }],
        password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
        password: [
          { required: true, message: '请输入密码', trigger: 'blur' },
          { validator: validateStrongPassword, trigger: 'blur' }
        ]
      },
      passwordRules: {
        password: [{ required: true, message: '请输入新密码', trigger: 'blur' }]
        password: [
          { required: true, message: '请输入新密码', trigger: 'blur' },
          { validator: validateStrongPassword, trigger: 'blur' }
        ]
      }
    }
  },
ruoyi-ui/src/views/visitor-admin/system/version.vue
@@ -31,6 +31,11 @@
              <div class="table-actions">
                <button type="button" @click="handleView(row)">查看</button>
                <button
                  v-if="!isMiddlePlatformReadonly && $auth.hasPermi('visitor:version:edit')"
                  type="button"
                  @click="handleEdit(row)"
                >编辑</button>
                <button
                  v-if="!row.latest && !isMiddlePlatformReadonly && $auth.hasPermi('visitor:version:remove')"
                  type="button"
                  @click="handleDelete(row)"
@@ -62,7 +67,7 @@
      </div>
    </el-dialog>
    <el-dialog title="新建" :visible.sync="open" width="482px" append-to-body custom-class="version-create-dialog">
    <el-dialog :title="form.updateId ? '编辑' : '新建'" :visible.sync="open" width="482px" append-to-body custom-class="version-create-dialog">
      <form class="version-form" @submit.prevent="submitForm">
        <label class="version-row required">
          <span>版本号:</span>
@@ -100,7 +105,7 @@
              <input ref="packageFile" type="file" hidden @change="handlePackageChange">
              <span><strong>{{ packageFileName ? packageFileName : '点击上传' }}</strong><i v-if="!packageFileName"> / </i>{{ packageFileName ? '' : '拖拽到此区域' }}</span>
            </button>
            <p class="upload-tip">请上传安装包文件,大小须小于200M</p>
            <p class="upload-tip">{{ form.updateId ? '不重新上传将保留原安装包,文件大小须小于200M' : '请上传安装包文件,大小须小于200M' }}</p>
          </div>
        </div>
@@ -120,8 +125,8 @@
</template>
<script>
import { listVersion, getVersion, addVersion, delVersion, uploadVersionPackage } from '@/api/visitor-admin/version'
import { getPageRows, getPageTotal, getResponseData } from '@/utils/visitor-admin'
import { listVersion, getVersion, addVersion, editVersion, delVersion, uploadVersionPackage } from '@/api/visitor-admin/version'
import { getPageRows, getPageTotal, getResponseData, normalizeBoolean } from '@/utils/visitor-admin'
const MAX_PACKAGE_SIZE = 200 * 1024 * 1024
@@ -167,13 +172,13 @@
      const value = String(versionNo || '').replace(/^[vV]/, '')
      return value ? `v${value}` : '-'
    },
    // 目的:打开新增版本弹窗;中台只读模式下禁止本地新增。
    // 目的:打开新增版本弹窗;非本地维护页面的中台只读模式下禁止本地新增。
    handleAdd() {
      if (this.isMiddlePlatformReadonly) {
        this.denyMiddlePlatformWrite()
        return
      }
      this.form = { forceUpdate: false, versionNo: '', updateContent: '', packageName: '', packageUrl: '' }
      this.form = { updateId: null, forceUpdate: false, versionNo: '', updateContent: '', packageName: '', packageUrl: '' }
      this.packageFile = null
      this.packageFileName = ''
      this.packageDragActive = false
@@ -181,6 +186,33 @@
        this.$refs.packageFile.value = ''
      }
      this.open = true
    },
    // 目的:读取完整版本信息后进入编辑态,未选择新文件时保留原安装包。
    handleEdit(row) {
      if (this.isMiddlePlatformReadonly) {
        this.denyMiddlePlatformWrite()
        return
      }
      getVersion(row.updateId).then(res => {
        const data = getResponseData(res) || {}
        this.form = {
          updateId: data.updateId,
          versionNo: String(data.versionNo || '').replace(/^[vV]/, ''),
          updateContent: data.updateContent || '',
          packageUrl: data.packageUrl || '',
          packageName: data.packageName || '',
          forceUpdate: normalizeBoolean(data.forceUpdate)
        }
        this.packageFile = null
        this.packageFileName = data.packageName || data.packageUrl || ''
        this.packageDragActive = false
        this.open = true
        this.$nextTick(() => {
          if (this.$refs.packageFile) {
            this.$refs.packageFile.value = ''
          }
        })
      })
    },
    // 目的:版本号只保留数字和小数点,支持多段版本号输入。
    handleVersionNoInput(event) {
@@ -217,46 +249,46 @@
          input.value = ''
        }
        this.packageFile = null
        this.packageFileName = ''
        this.form.packageName = ''
        this.form.packageUrl = ''
        this.packageFileName = this.form.updateId ? (this.form.packageName || this.form.packageUrl || '') : ''
        return
      }
      this.packageFile = file || null
      this.packageFileName = file ? file.name : ''
      if (file) {
        this.form.packageName = file.name
        this.form.packageUrl = ''
      }
    },
    // 目的:校验并提交新增版本;中台只读模式下禁止提交。
    // 目的:统一校验并提交新增或编辑版本,编辑时仅在重新选择文件后上传安装包。
    submitForm() {
      if (this.isMiddlePlatformReadonly) {
        this.denyMiddlePlatformWrite()
        return
      }
      if (!/^\d+(\.\d+)*$/.test(this.form.versionNo || '') || !this.packageFile) {
      const isEdit = Boolean(this.form.updateId)
      if (!/^\d+(\.\d+)*$/.test(this.form.versionNo || '') || (!this.packageFile && !this.form.packageUrl)) {
        this.$modal.msgError('请填写版本号并上传安装包')
        return
      }
      if (this.packageFile.size >= MAX_PACKAGE_SIZE) {
      if (this.packageFile && this.packageFile.size >= MAX_PACKAGE_SIZE) {
        this.$modal.msgError('操作失败:安装包大小必须小于200M')
        return
      }
      const uploadRequest = this.packageFile ? this.uploadPackage() : Promise.resolve()
      uploadRequest.then(() => isEdit ? editVersion(this.form) : addVersion(this.form)).then(() => {
        this.$modal.msgSuccess(isEdit ? '编辑成功' : '新增成功')
        this.open = false
        this.getList()
      })
    },
    // 目的:上传当前选择的安装包,并把服务端文件信息回填到版本表单。
    uploadPackage() {
      const packageFile = this.packageFile
      const formData = new FormData()
      formData.append('file', this.packageFile)
      uploadVersionPackage(formData).then(res => {
      formData.append('file', packageFile)
      return uploadVersionPackage(formData).then(res => {
        const data = getResponseData(res) || {}
        this.form.packageUrl = data.fileName || data.url || ''
        this.form.packageName = data.originalFilename || data.newFileName || this.packageFile.name
        this.form.packageName = data.originalFilename || data.newFileName || packageFile.name
        if (!this.form.packageUrl) {
          throw new Error('安装包上传失败,未返回文件地址')
        }
        return addVersion(this.form)
      }).then(() => {
        this.$modal.msgSuccess('新增成功')
        this.open = false
        this.getList()
      })
    },
    // 目的:查看任意版本的完整更新信息。
ruoyi-ui/vue.config.js
@@ -18,7 +18,10 @@
  // 部署生产环境和开发环境下的URL。
  // 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上
  // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
  publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
  // 使用相对路径,根目录和子目录部署均可直接加载静态资源。
  publicPath: process.env.NODE_ENV === "production"
    ? (process.env.VUE_APP_PUBLIC_PATH || "./")
    : "/",
  // 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist)
  outputDir: 'dist',
  // 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下)