zjk
2026-07-24 3ee306e4a8db75680960f5eb4eec9fb7d05b5f29
Merge branch 'master' into electron_dev
6个文件已添加
75个文件已修改
1650 ■■■■■ 已修改文件
doc/sql/20260706_t_visitor_machine.sql 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysVisitorRecordController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-test.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/RuoYiAppletApplication.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/EmployeeVerifyController.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiDoorMachineController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiProtocolConfigController.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiVisitorConfigController.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiVisitorRecordController.java 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiVisitorRecordManageController.java 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/ApiPullSignOutDataService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/ApiPullSingInDataService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/VisitorRegisterSyncService.java 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/java/com/ruoyi/web/controller/tool/ImageUtil.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-applet/src/main/resources/application-test.yml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/Employee.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitArea.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorMachine.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorRecord.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorSignoutTemp.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/api/ApiSysVisitorRecord.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/AreasList.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/ApiVisitorMachineRequest.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/ApiVisitorRecodeSingoutRequest.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/ApiVisitorRecordSinginRequest.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/EmployeeDto.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysVisitorIdCardOutRequest.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysVisitorRecordAddRequest.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/EmployeeMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysVersionUpdateMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysVisitAreaMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysVisitorRecordMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/api/SysApiVisitorRecordMapper.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysVersionUpdateService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysVisitorMachineService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysVisitorRecordService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVersionUpdateServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitAreaServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitorConfigServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitorMachineServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitorRecordServiceImpl.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVisitorMachineVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/EmployeeMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysVersionUpdateMapper.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysVisitAreaMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysVisitorMachineMapper.xml 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysVisitorRecordMapper.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/.env.development 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/.env.production 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/api/visitor-admin/record.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/assets/styles/visitor-admin.scss 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/components/ImageUpload/index.vue 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/layout/components/Navbar.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/utils/request.js 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/visitor-admin/device/index.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/visitor-admin/organization/employee.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/visitor-admin/record/detail.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/visitor-admin/record/visitor.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/visitor-admin/system/config.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/visitor-admin/system/version.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/vue.config.js 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/css/version-update.css 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/css/visitor-machine.css 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/js/role-form.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/js/version-update.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/js/visitor-machine.js 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/js/visitor-record.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/dashboard.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/department-management.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/door-management.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/employee-management.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/platform-admin.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/protocol-config.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/role-form.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/role-management.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/system-config.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/version-update.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/visitor-machine.html 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/visitor-record-detail.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
visitor-admin-ui/pages/visitor-record.html 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/sql/20260706_t_visitor_machine.sql
@@ -7,6 +7,7 @@
  `device_id` varchar(100) NOT NULL COMMENT '设备ID',
  `enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用',
  `online_status` varchar(32) NOT NULL DEFAULT 'UNACTIVATED' COMMENT '在线状态:UNACTIVATED未激活 ONLINE在线 OFFLINE离线',
  `fingerprint` varchar(255) DEFAULT NULL COMMENT '百度SDK指纹',
  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标识,0正常 1删除',
  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysVisitorRecordController.java
@@ -24,6 +24,8 @@
@RequestMapping("/system/visitor/record")
public class SysVisitorRecordController extends BaseController
{
    private static final int EXPORT_PAGE_SIZE = 10000;
    @Autowired
    private ISysVisitorRecordService visitorRecordService;
@@ -39,6 +41,20 @@
    }
    /**
     * 导出访客登记记录
     */
    @Log(title = "访客登记记录", businessType = BusinessType.EXPORT)
    @PreAuthorize("@ss.hasPermi('visitor:record:export')")
    @PostMapping("/export")
    public AjaxResult export(@RequestBody(required = false) SysVisitorRecordPageRequest request)
    {
        SysVisitorRecordPageRequest query = request == null ? new SysVisitorRecordPageRequest() : request;
        query.setPageNum(1);
        query.setPageSize(EXPORT_PAGE_SIZE);
        return success(visitorRecordService.pageList(query).getRecords());
    }
    /**
     * 查看访客登记详情
     */
    @Log(title = "访客登记记录", businessType = BusinessType.SELECT)
ruoyi-admin/src/main/resources/application-test.yml
@@ -17,7 +17,7 @@
  # 部署模式:standalone单机版 cluster集群版
  deploymentMode: cluster
  # 集群版是否对接中台:true对接 false不对接
  middlePlatformEnabled: true
  middlePlatformEnabled: false
# 开发环境配置
server:
  # 服务器的HTTP端口,默认为8080
ruoyi-applet/src/main/java/com/ruoyi/RuoYiAppletApplication.java
@@ -29,8 +29,6 @@
public class RuoYiAppletApplication
{
    public static void main(String[] args) {
        ConfigurableApplicationContext application = SpringApplication.run(RuoYiAppletApplication.class, args);
        try {
            Environment env = application.getEnvironment();
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/EmployeeVerifyController.java
@@ -16,6 +16,7 @@
import com.ruoyi.system.mapper.SysVisitorMachineMapper;
import com.ruoyi.system.service.EmployeeVerifyService;
import com.ruoyi.system.service.ISysVisitorMachineService;
import com.ruoyi.system.utils.EmployeePhoneCrypto;
import com.ruoyi.system.vo.SysVisitorMachineVO;
import com.ruoyi.web.controller.tool.ImageUtil;
import io.swagger.annotations.Api;
@@ -62,12 +63,12 @@
     * 刷卡验证:现场读卡器读取卡号后调用本接口,与接待人绑定的卡号比对。
     */
    @PostMapping("/allEmployee")
    @ApiOperation(value = "查询所有员工接口", notes = "必传设备ID")
    public AjaxResult getAllEmployee(@RequestBody @Valid ApiVisitorMachineRequest request) {
        SysVisitorMachine detail = visitorMachineMapper.checkDeviceIdUnique(request.getMachineId(),null);
        if (detail == null || !detail.getEnabled()) {
            throw new ServiceException("未找到有效设备");
        }
    @ApiOperation(value = "查询所有员工接口")
    public AjaxResult getAllEmployee() {
//        SysVisitorMachine detail = visitorMachineMapper.checkDeviceIdUnique(request.getMachineId(),null);
//        if (detail == null || !detail.getEnabled()) {
//            throw new ServiceException("未找到有效设备");
//        }
        List<Employee> employees = employeeMapper.selectAllEnabled();
        if (employees == null || employees.isEmpty()) {
@@ -85,6 +86,12 @@
                        dto.setFaceImage(base64);
                    }
                    if (StringUtils.isNotEmpty(employee.getContactPhoneCipher())) {
                        String phone = EmployeePhoneCrypto.decrypt(employee.getContactPhoneCipher());
                        dto.setPhone(phone);
                    }
                    dto.setContactPhoneCipher(null);
                    return dto;
                })
                .collect(Collectors.toList());
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiDoorMachineController.java
@@ -4,6 +4,8 @@
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.dto.*;
import com.ruoyi.system.service.ISysVisitorMachineService;
import io.swagger.annotations.Api;
@@ -36,7 +38,11 @@
    @PostMapping("/activate")
    @ApiOperation(value = "激活访客机设备")
    public AjaxResult activate(@Valid @RequestBody ApiVisitorMachineRequest request) {
        int result = visitorMachineService.activateDevice(request.getMachineId());
        if (StringUtils.isEmpty(request.getFingerprint())) {
            throw new ServiceException("缺少参数");
        }
        int result = visitorMachineService.activateDevice(request.getMachineId(), request.getFingerprint());
        return result > 0 ? success("设备激活成功") : error("设备激活失败");
    }
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiProtocolConfigController.java
New file
@@ -0,0 +1,51 @@
package com.ruoyi.web.controller.api;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.dto.SysProtocolConfigDetailRequest;
import com.ruoyi.system.dto.SysProtocolConfigSaveRequest;
import com.ruoyi.system.service.ISysProtocolConfigService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 入园承诺
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/api/admission")
@Api(tags = "入园承诺" , value = "入园承诺")
public class SysApiProtocolConfigController extends BaseController
{
    @Autowired
    private ISysProtocolConfigService protocolConfigService;
    /**
     * 入园承诺
     */
    @Log(title = "入园承诺", businessType = BusinessType.SELECT)
    @PostMapping("/detail")
    @ApiOperation(value = "入园承诺")
    @ApiResponses({
            @ApiResponse(code = 200, message = "操作成功", response = SysProtocolConfigDetailRequest.class),
            @ApiResponse(code = 500, message = "操作失败")
    })
    public AjaxResult<SysProtocolConfigDetailRequest> detail()
    {
        SysProtocolConfigDetailRequest request = new SysProtocolConfigDetailRequest();
        request.setProtocolType("ENTRY_COMMITMENT");//入园承诺
        return success(protocolConfigService.selectConfig(request));
    }
}
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiVisitorConfigController.java
New file
@@ -0,0 +1,53 @@
package com.ruoyi.web.controller.api;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.dto.SysVisitorConfigSaveRequest;
import com.ruoyi.system.service.ISysVisitorConfigService;
import com.ruoyi.system.vo.SysVisitorConfigVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 访客系统配置接口
 *
 * @author ruoyi
 */
@RestController
@RequestMapping("/api/visitorconfig")
@Api(tags = "访客系统配置接口")
public class SysApiVisitorConfigController extends BaseController
{
    @Autowired
    private ISysVisitorConfigService visitorConfigService;
    /**
     * 查询当前配置
     */
    @Log(title = "访客系统配置", businessType = BusinessType.SELECT)
    @PostMapping("/detail")
    @ApiOperation(
            value = "查询访客系统配置",
            notes = "获取当前访客系统的配置信息,包括访客登记、审批流程等设置",
            response = SysVisitorConfigVO.class
    )
    @ApiResponses({
            @ApiResponse(code = 200, message = "查询成功", response = SysVisitorConfigVO.class),
            @ApiResponse(code = 500, message = "查询失败")
    })
    public AjaxResult<SysVisitorConfigVO> detail()
    {
        SysVisitorConfigVO data = visitorConfigService.selectConfig();
        return new AjaxResult<>(200, "操作成功", data);
    }
}
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiVisitorRecordController.java
@@ -1,6 +1,8 @@
package com.ruoyi.web.controller.api;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -12,6 +14,8 @@
import com.ruoyi.system.domain.Employee;
import com.ruoyi.system.domain.SysVisitArea;
import com.ruoyi.system.domain.SysVisitorRecord;
import com.ruoyi.system.domain.vo.AreasList;
import com.ruoyi.system.dto.SysVisitAreaTreeRequest;
import com.ruoyi.system.dto.SysVisitorIdCardOutRequest;
import com.ruoyi.system.dto.SysVisitorRecordAddRequest;
import com.ruoyi.system.mapper.EmployeeMapper;
@@ -19,26 +23,29 @@
import com.ruoyi.system.mapper.SysVisitAreaMapper;
import com.ruoyi.system.mapper.SysVisitorRecordMapper;
import com.ruoyi.system.service.ISysVisitAreaService;
import com.ruoyi.system.utils.EmployeePhoneCrypto;
import com.ruoyi.system.vo.SysVisitAreaVO;
import com.ruoyi.web.controller.service.SysVisitorRecordService;
import com.ruoyi.web.controller.service.VisitorRegisterSyncService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/visitorRecord")
@Api(tags = "现场访客登记-签离")
public class SysApiVisitorRecordController extends BaseController {
    @Autowired
    private VisitorRegisterSyncService visitorRegisterSyncService;
    @Autowired
    private SysVisitorRecordService sysVisitorRecordService;
    @Autowired
@@ -52,6 +59,7 @@
    @PostMapping("/add")
    @ApiOperation(value = "访客登记", notes = "现场访客登记")
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult add(
            @RequestBody
            @ApiParam(value = "访客登记信息", required = true)
@@ -64,6 +72,21 @@
        if (StringUtils.isBlank(request.getIntervieweeName())) {
            return AjaxResult.error("被访人不能为空");
        }
        List<Employee> employeeISList = employeeMapper.selectEnabledByEmployeeName(request.getIntervieweeName());
        if (employeeISList.isEmpty()) {
            throw new ServiceException("被访人不存在,请联系工作人员");
        }else {
            //重名
            if (StringUtils.isNotEmpty(request.getBeVisitorPhone())) {
                String phoneHash = StringUtils.isEmpty(request.getBeVisitorPhone()) ? null : EmployeePhoneCrypto.hash(request.getBeVisitorPhone());
                Employee employee = employeeMapper.selectOne(new LambdaQueryWrapper<Employee>().eq(Employee::getContactPhoneHash, phoneHash));
                if (employee == null) {
                    throw new ServiceException("被访人不存在,请联系工作人员");
                }
            }
        }
        // 签离的当天不能再次进入 签到
@@ -103,15 +126,42 @@
        BeanUtils.copyProperties(request, record);
        //查询地区
        if (StringUtils.isNotEmpty(record.getVisitAreaIds())) {
            String[] visitAreaIds = record.getVisitAreaIds().split(",");
        List<AreasList> areasList = null;
        //查询地区
        if (StringUtils.isNotEmpty(request.getVisitAreaIds())) {
            String[] visitAreaIds = request.getVisitAreaIds().split(",");
            String visitArea = Arrays.stream(visitAreaIds)
                    .map(id -> {
                        SysVisitArea area = sysVisitAreaMapper.selectById(Long.parseLong(id.trim()));
                        if (area == null || "1".equals(area.getDelFlag())) {
                            throw new ServiceException("访问区域不存在,请联系工作人员");
                        }
                        return area != null ? area.getAreaName() : null;
                    })
                    .filter(Objects::nonNull)
                    .collect(Collectors.joining(","));
            // ====== 新增:构建 areas 数组 ======
            areasList = Arrays.stream(visitAreaIds)
                    .map(String::trim)
                    .filter(StringUtils::isNotEmpty)
                    .map(id -> {
                        SysVisitArea area = sysVisitAreaMapper.selectById(Long.parseLong(id));
                        if (area == null) {
                            throw new ServiceException("访问区域不存在,请联系工作人员");
                        }
                        AreasList areas = new AreasList();
                        areas.setAreaCode(area.getAreaCode());
                        areas.setAreaName(area.getAreaName());
                        return areas;
                    })
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
            if (!areasList.isEmpty()) {
                record.setAreasList(areasList);
            }
            // 使用 visitArea
            if (StringUtils.isNotEmpty(visitArea)) {
@@ -120,37 +170,112 @@
            }
        }
        // 如果 visitAreaIds 为空,但 visitAreaNames 不为空,根据名称查询填充 visitAreaIds
        else if (StringUtils.isNotEmpty(request.getVisitAreaNames())) {
            String[] visitAreaNames = request.getVisitAreaNames().split(",");
            // 根据区域名称查询对应的ID,构建ID字符串
            String visitAreaIdsStr = Arrays.stream(visitAreaNames)
                    .map(String::trim)
                    .filter(StringUtils::isNotEmpty)
                    .map(name -> {
                        // 根据区域名称查询区域信息(这里假设您有根据名称查询的方法)
                        SysVisitAreaTreeRequest query = new SysVisitAreaTreeRequest();
                        query.setAreaName(name);
                        List<SysVisitAreaVO> area = sysVisitAreaMapper.selectAreaTreeList(query);
                        return area != null ? String.valueOf(area.get(0).getAreaId()) : null;
                    })
                    .filter(Objects::nonNull)
                    .collect(Collectors.joining(","));
            if (StringUtils.isNotEmpty(visitAreaIdsStr)) {
                record.setVisitAreaIds(visitAreaIdsStr);
            }
            // 同时构建 areas 数组
            areasList = Arrays.stream(visitAreaNames)
                    .map(String::trim)
                    .filter(StringUtils::isNotEmpty)
                    .map(name -> {
                        SysVisitAreaTreeRequest query = new SysVisitAreaTreeRequest();
                        query.setAreaName(name);
                        List<SysVisitAreaVO> area = sysVisitAreaMapper.selectAreaTreeList(query);
                        if (area == null || area.isEmpty()) {
                            return null;
                        }
                        AreasList areas = new AreasList();
                        areas.setAreaCode(area.get(0).getAreaCode());
                        areas.setAreaName(area.get(0).getAreaName());
                        return areas;
                    })
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
            if (!areasList.isEmpty()) {
                record.setAreasList(areasList);
                record.setRemark("来访中,允许访问区域:" + record.getVisitAreaNames());
            }
        }
        //查询接待人的部门
        if (StringUtils.isNotEmpty(record.getIntervieweeName())) {
            Employee employee = employeeMapper.selectEnabledByEmployeeName(record.getIntervieweeName());
            if (employee != null && employee.getDeptId() != null) {
                SysDept sysDept = sysDeptMapper.selectDeptById(employee.getDeptId());
                if (sysDept != null) {
                    record.setIntervieweeDept(sysDept.getDeptName());
            List<Employee> employee = employeeMapper.selectEnabledByEmployeeName(record.getIntervieweeName());
            if (!employee.isEmpty()) {
                Employee employeeNo1 = employee.get(0);
                if (employeeNo1.getDeptId() != null) {
                    SysDept sysDept = sysDeptMapper.selectDeptById(employeeNo1.getDeptId());
                    if (sysDept != null) {
                        record.setIntervieweeDept(sysDept.getDeptName());
                    }
                }
            }
        }
        record.setRegisterNo(generateRegisterNo());//登记编号
        record.setRegisterType("ONSITE");//现场登记
//        record.setRegisterType("ONSITE");//现场登记
        record.setRegisterSource("访客机");
        record.setStatus("VISITING");//来访中
        record.setRegisterTime(new Date());
        record.setCreateTime(new Date());
        record.setDelFlag("0");
        // ================= 同步到对端服务器 =================
        try {
            AjaxResult syncResult = visitorRegisterSyncService.syncVisitorRegister(record);
            if (syncResult.isSuccess()) {
                logger.info("访客登记数据已同步到对端,远程ID:{}", syncResult.get("data"));
                // 可将远程ID保存到本地记录
//                record.setRemoteId(syncResult.getRemoteId());
                UpdateWrapper<SysVisitorRecord> wrapper = new UpdateWrapper<>();
                wrapper.eq("record_id", record.getRecordId());  // 指定ID条件
                visitorRecordMapper.update(record, wrapper);
            } else {
                String msg = (String) syncResult.get("msg");
                logger.error("访客登记同步失败:{}", msg);
                // 可选择是否允许本地登记成功但同步失败
                // 如果要保证数据一致性,这里可以抛出异常回滚事务
                throw new ServiceException(msg);
            }
        } catch (Exception e) {
            logger.error("访客登记同步异常", e);
            throw new ServiceException("服务器登记失败");
        }
        int rows = visitorRecordMapper.insert(record);
        sysVisitorRecordService.registerVisitor(record);
        if (rows > 0) {
            return AjaxResult.success(record);
        }
        return AjaxResult.error("登记失败");
    }
    @PostMapping("/idCardSingOut")
    @ApiOperation(value = "访客身份证签离", notes = "访客身份证签离")
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult idCardSingOut(
            @RequestBody
            @ApiParam(value = "访客身份证签离", required = true)
@@ -187,15 +312,17 @@
        } else {
            record.setStatus("SIGNED_OUT");
        }
        record.setSignVerifyMethod("MANUAL");
        SysVisitorRecord visitorRecord = new SysVisitorRecord();
        BeanUtils.copyBeanProp(visitorRecord, request);
        visitorRecord.setSignVerifyMethod(record.getSignVerifyMethod());
        visitorRecord.setSignVerifyMethod(request.getSignVerifyMethod());
        visitorRecord.setStatus(record.getStatus());
        visitorRecord.setRecordId(record.getRecordId());
        visitorRecord.setSignTime(new Date());
        visitorRecordMapper.updateById(visitorRecord);
        UpdateWrapper<SysVisitorRecord> wrapperUpdate = new UpdateWrapper<>();
        wrapperUpdate.eq("record_id", visitorRecord.getRecordId());  // 指定ID条件
        visitorRecordMapper.update(visitorRecord, wrapperUpdate);
        return success();
    }
ruoyi-applet/src/main/java/com/ruoyi/web/controller/api/SysApiVisitorRecordManageController.java
@@ -5,18 +5,33 @@
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.system.dto.SysVisitorRecordDetailRequest;
import com.ruoyi.system.dto.SysVisitorRecordPageRequest;
import com.ruoyi.system.service.ISysVersionUpdateService;
import com.ruoyi.system.service.ISysVisitorRecordService;
import com.ruoyi.system.vo.SysVersionUpdateVO;
import com.ruoyi.system.vo.SysVisitorRecordDetailVO;
import com.ruoyi.system.vo.SysVisitorRecordVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.List;
/**
 * 管理侧访客记录接口
@@ -30,6 +45,9 @@
{
    @Autowired
    private ISysVisitorRecordService visitorRecordService;
    @Autowired
    private ISysVersionUpdateService versionUpdateService;
    /**
     * 分页查询访客记录
@@ -55,6 +73,135 @@
    }
    /**
     * 导出访客记录
     */
    @ApiOperation(
            value = "导出访客记录",
            notes = "按分页查询接口的筛选条件查询全部访客记录并导出 Excel,不进行分页"
    )
    @PostMapping("/export")
    public void export(HttpServletResponse response,
                       @RequestBody(required = false) SysVisitorRecordPageRequest request) throws IOException
    {
        List<SysVisitorRecordVO> records = visitorRecordService.list(request);
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode("访客记录", "UTF-8").replace("+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
        SXSSFWorkbook workbook = new SXSSFWorkbook(500);
        try
        {
            writeVisitorRecordSheet(workbook, records);
            workbook.write(response.getOutputStream());
            response.flushBuffer();
        }
        finally
        {
            workbook.dispose();
            workbook.close();
        }
    }
    private void writeVisitorRecordSheet(SXSSFWorkbook workbook, List<SysVisitorRecordVO> records)
    {
        String[] headers = {
                "登记编号", "访客姓名", "证件号码", "登记类型", "被访人", "被访部门", "访问区域",
                "车牌号", "来访事由", "有效期开始时间", "有效期结束时间", "状态", "登记时间"
        };
        int[] columnWidths = {20, 16, 24, 16, 16, 20, 24, 16, 24, 22, 22, 20, 22};
        Sheet sheet = workbook.createSheet("访客记录");
        sheet.createFreezePane(0, 1);
        CellStyle headerStyle = createHeaderStyle(workbook);
        Row headerRow = sheet.createRow(0);
        for (int index = 0; index < headers.length; index++)
        {
            Cell cell = headerRow.createCell(index);
            cell.setCellValue(headers[index]);
            cell.setCellStyle(headerStyle);
            sheet.setColumnWidth(index, columnWidths[index] * 256);
        }
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        for (int index = 0; index < records.size(); index++)
        {
            SysVisitorRecordVO record = records.get(index);
            Row row = sheet.createRow(index + 1);
            setCellValue(row, 0, record.getRegisterNo());
            setCellValue(row, 1, record.getVisitorName());
            setCellValue(row, 2, record.getIdCard());
            setCellValue(row, 3, registerTypeName(record.getRegisterType()));
            setCellValue(row, 4, record.getIntervieweeName());
            setCellValue(row, 5, record.getIntervieweeDept());
            setCellValue(row, 6, record.getVisitAreaNames());
            setCellValue(row, 7, record.getPlateNo());
            setCellValue(row, 8, record.getVisitReason());
            setCellValue(row, 9, formatDate(dateFormat, record.getValidStartTime()));
            setCellValue(row, 10, formatDate(dateFormat, record.getValidEndTime()));
            setCellValue(row, 11, statusName(record.getStatus()));
            setCellValue(row, 12, formatDate(dateFormat, record.getRegisterTime()));
        }
    }
    private CellStyle createHeaderStyle(SXSSFWorkbook workbook)
    {
        Font font = workbook.createFont();
        font.setBold(true);
        CellStyle style = workbook.createCellStyle();
        style.setFont(font);
        style.setAlignment(HorizontalAlignment.CENTER);
        return style;
    }
    private void setCellValue(Row row, int columnIndex, String value)
    {
        row.createCell(columnIndex).setCellValue(value == null ? "" : value);
    }
    private String formatDate(SimpleDateFormat dateFormat, java.util.Date date)
    {
        return date == null ? "" : dateFormat.format(date);
    }
    private String registerTypeName(String registerType)
    {
        if ("APPOINTMENT".equalsIgnoreCase(registerType))
        {
            return "预约登记";
        }
        if ("ONSITE".equalsIgnoreCase(registerType))
        {
            return "现场登记";
        }
        return registerType;
    }
    private String statusName(String status)
    {
        if ("VISITING".equalsIgnoreCase(status))
        {
            return "来访中";
        }
        if ("SIGNED_OUT".equalsIgnoreCase(status))
        {
            return "已签离";
        }
        if ("PENDING_CONFIRM".equalsIgnoreCase(status))
        {
            return "逾期滞留";
        }
        if ("OVERDUE_AUTO_SIGNED_OUT".equalsIgnoreCase(status))
        {
            return "超期自动签离";
        }
        if ("CANCELLED".equalsIgnoreCase(status))
        {
            return "已取消";
        }
        return status;
    }
    /**
     * 查询访客记录详情
     */
    @ApiOperation(
@@ -75,4 +222,26 @@
    {
        return success(visitorRecordService.detail(request));
    }
    /**
     * 获取最新版本信息
     */
    @ApiOperation(
            value = "获取最新版本信息",
            notes = "返回最新一条未删除的版本更新记录",
            response = SysVersionUpdateVO.class
    )
    @ApiResponses({
            @ApiResponse(
                    code = 200,
                    message = "成功",
                    response = AjaxResult.class,
                    responseContainer = "Data"
            )
    })
    @PostMapping("/latestVersion")
    public AjaxResult<SysVersionUpdateVO> latestVersion()
    {
        return success(versionUpdateService.latest());
    }
}
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/ApiPullSignOutDataService.java
@@ -63,7 +63,7 @@
                // 1. 参数校验
                if (!StringUtils.hasText(request.getIdCard())) {
                    log.warn("身份证号为空,已跳过,设备编号: {}", request.getDeviceNo());
                    log.warn("身份证号为空,已跳过,人员姓名: {}", request.getIntervieweeName());
                    errorCount++;
                    continue;
                }
@@ -73,12 +73,12 @@
                    request.setSignImageUrl(url);
                }
                // 2. 防重校验(基于设备编号+身份证号+签离时间)
                // 2. 防重校验(身份证号+签离时间)
                String uniqueKey = generateUniqueKey(request);
                String redisKey = "offline:signout:" + uniqueKey;
                if (Boolean.TRUE.equals(redisTemplate.hasKey(redisKey))) {
                    log.warn("签离数据重复,已跳过, 设备编号: {}, 身份证号: {}, 签离时间: {}",
                            request.getDeviceNo(), request.getIdCard(), request.getSignTime());
                    log.warn("签离数据重复,已跳过, 身份证号: {}, 签离时间: {}",
                            request.getIdCard(), request.getSignTime());
                    skipCount++;
                    continue;
                }
@@ -94,8 +94,8 @@
                }
            } catch (Exception e) {
                log.error("处理签离数据失败, 身份证号: {}, 设备编号: {}",
                        request.getIdCard(), request.getDeviceNo(), e);
                log.error("处理签离数据失败, 身份证号: {}",
                        request.getIdCard(), e);
                errorCount++;
            }
        }
@@ -362,7 +362,7 @@
     */
    private String generateUniqueKey(ApiVisitorRecodeSingoutRequest request) {
        StringBuilder sb = new StringBuilder();
        sb.append(request.getDeviceNo()).append(":");
//        sb.append(request.getDeviceNo()).append(":");
        sb.append(request.getIdCard()).append(":");
        sb.append(request.getSignTime() != null ?
                request.getSignTime().getTime() : System.currentTimeMillis());
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/ApiPullSingInDataService.java
@@ -59,7 +59,7 @@
                // 1. 参数校验
                if (!StringUtils.hasText(request.getIdCard())) {
                    log.warn("身份证号为空,已跳过,设备编号: {}", request.getDeviceNo());
                    log.warn("身份证号为空,已跳过,访问人姓名: {}", request.getVisitorName());
                    errorCount++;
                    continue;
                }
@@ -73,8 +73,8 @@
                String uniqueKey = generateUniqueKey(request);
                String redisKey = "offline:signin:" + uniqueKey;
                if (Boolean.TRUE.equals(redisTemplate.hasKey(redisKey))) {
                    log.warn("签到数据重复,已跳过, 设备编号: {}, 身份证号: {}, 登记时间: {}",
                            request.getDeviceNo(), request.getIdCard(), request.getRegisterTime());
                    log.warn("签到数据重复,已跳过, 身份证号: {}, 登记时间: {}",
                            request.getIdCard(), request.getRegisterTime());
                    skipCount++;
                    continue;
                }
@@ -135,7 +135,7 @@
        BeanUtils.copyProperties(request, record);
        record.setRegisterTime(registerTime);
        record.setRegisterSource("访客机");
        record.setRegisterType("ONSITE");
//        record.setRegisterType("ONSITE");
        record.setCreateTime(new Date());
        record.setDelFlag("0");
        record.setRegisterNo(generateRegisterNo());
@@ -148,8 +148,6 @@
        // 4. 如果有未关联的签离数据,直接关联
        if (signoutTemp != null) {
            // 设置签离信息
            record.setSignTime(signoutTemp.getSignTime());
            record.setSignReceiverName(signoutTemp.getSignReceiverName());
@@ -321,7 +319,7 @@
     */
    private String generateUniqueKey(ApiVisitorRecordSinginRequest request) {
        StringBuilder sb = new StringBuilder();
        sb.append(request.getDeviceNo()).append(":");
//        sb.append(request.getDeviceNo()).append(":");
        sb.append(request.getIdCard()).append(":");
        sb.append(request.getRegisterTime() != null ?
                request.getRegisterTime().getTime() : System.currentTimeMillis());
ruoyi-applet/src/main/java/com/ruoyi/web/controller/service/VisitorRegisterSyncService.java
New file
@@ -0,0 +1,191 @@
package com.ruoyi.web.controller.service;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.ContentType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysVisitorRecord;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
 * 访客登记同步服务
 * 将本地访客登记数据同步到对端服务器
 */
@Slf4j
@Service
public class VisitorRegisterSyncService {
    @Value("${visitor.sync.url:}")
    private String syncUrl;
    @Value("${visitor.sync.enabled:false}")
    private Boolean syncEnabled;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final DateTimeFormatter DATE_FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    /**
     * 同步访客登记信息到对端服务器
     *
     * @param record 本地访客登记记录
     * @return 同步结果
     */
    public AjaxResult syncVisitorRegister(SysVisitorRecord record) {
        // 检查同步开关
        if (!syncEnabled) {
            log.info("访客登记同步已关闭,跳过同步");
            return AjaxResult.success("同步功能未开启");
        }
        if (StringUtils.isBlank(syncUrl)) {
            log.warn("同步URL未配置,跳过同步");
            return AjaxResult.warn("同步URL未配置");
        }
        try {
            // 1. 构建同步请求参数
            Map<String, Object> requestBody = buildSyncRequest(record);
            String jsonBody = objectMapper.writeValueAsString(requestBody);
            log.info("同步访客登记数据:{}", jsonBody);
            // 2. 发送HTTP请求
            HttpResponse response = HttpRequest.post(syncUrl)
                    .header("Content-Type", ContentType.JSON.getValue())
                    .body(jsonBody)
                    .timeout(30000)
                    .execute();
            // 3. 解析响应
            String responseBody = response.body();
            log.info("同步响应:{}", responseBody);
            JsonNode jsonNode = objectMapper.readTree(responseBody);
            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;
                log.info("访客登记同步成功,远程ID:{}", remoteId);
                return AjaxResult.success(remoteId);
            } else {
                String errorMsg = jsonNode.has("description")
                        ? jsonNode.get("description").asText() : "未知错误";
                log.error("访客登记同步失败:{}", errorMsg);
                return AjaxResult.error(errorMsg);
            }
        } catch (Exception e) {
            log.error("访客登记同步异常", e);
            return AjaxResult.error("同步异常:" + e.getMessage());
        }
    }
    /**
     * 构建同步请求参数
     */
    private Map<String, Object> buildSyncRequest(SysVisitorRecord record) {
        Map<String, Object> params = new HashMap<>();
        // 必填字段
        params.put("name", record.getVisitorName());
        params.put("certType", "111");
        params.put("certNo", record.getIdCard());
//        params.put("qrCode", generateQrCode(record));
        params.put("person", record.getIntervieweeName());
        params.put("reason", StringUtils.isNotBlank(record.getVisitReason())
                ? record.getVisitReason() : "来访");
        params.put("estimatedTimeStart", formatTime(record.getValidStartTime()));
        params.put("estimatedTimeEnd", formatTime(record.getValidEndTime()));
        if (record.getAreasList() != null && !record.getAreasList().isEmpty()) {
            params.put("areas", record.getAreasList());
        }
        // 可选字段
//        params.put("gender", convertGender(record.getGender()));
//        params.put("phone", record.getPhone());
        params.put("licensePlateNumber", record.getPlateNo());
//        params.put("visitorDept", record.getVisitorCompany());
        params.put("dept", record.getIntervieweeDept());
        params.put("areaName", record.getVisitAreaNames());
        params.put("registerGate", record.getRegisterSource());
        params.put("registerMachineCode", record.getDeviceId());
        params.put("status", "1"); // 1-已登记
        // 如果有证件照片或现场照片,可以传递
        // params.put("idPhoto", record.getIdCardPhoto());
         params.put("pic", record.getFaceImageUrl());
        return params;
    }
    /**
     * 转换证件类型:本地字典 → 对方字典值
     */
    private String convertCertType(String localCertType) {
        if (StringUtils.isBlank(localCertType)) {
            return "111"; // 默认身份证
        }
        // 根据实际情况映射
        switch (localCertType) {
            case "ID_CARD": return "111";      // 居民身份证
            case "PASSPORT": return "414";     // 护照
            case "DRIVER_LICENSE": return "335"; // 驾驶证
            case "HK_MACAU_PASS": return "337";  // 港澳通行证
            default: return "111";
        }
    }
    /**
     * 转换性别:本地字典 → 对方字典值
     */
    private String convertGender(String localGender) {
        if (StringUtils.isBlank(localGender)) {
            return null;
        }
        switch (localGender) {
            case "MALE": return "1";
            case "FEMALE": return "2";
            default: return null;
        }
    }
    /**
     * 格式化时间
     */
    private String formatTime(Date date) {
        if (date == null) {
            return null;
        }
        LocalDateTime localDateTime = date.toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDateTime();
        return localDateTime.format(DATE_FORMATTER);
    }
    /**
     * 生成二维码内容
     * 格式:vms:visitor:{日期}:{登记编号}
     */
    private String generateQrCode(SysVisitorRecord record) {
        String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String registerNo = record.getRegisterNo();
        return String.format("vms:visitor:%s:%s", dateStr, registerNo);
    }
}
ruoyi-applet/src/main/java/com/ruoyi/web/controller/tool/ImageUtil.java
@@ -1,14 +1,19 @@
package com.ruoyi.web.controller.tool;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.utils.file.FileUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
@Slf4j
@Component
@@ -25,20 +30,21 @@
            return null;
        }
        String imagePath = RuoYiConfig.getUploadPath() + imageUrl.replace("/profile/upload", "");
        InputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        try {
            // 1. 创建URL连接
            URL url = new URL(imageUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(5000);
            connection.setReadTimeout(10000);
            connection.setDoInput(true);
            // 1. 获取本地文件
            File imageFile = new File(imagePath);
            if (!imageFile.exists()) {
                log.error("图片文件不存在:{}", imagePath);
                return null;
            }
            // 2. 获取图片数据
            inputStream = connection.getInputStream();
            // 2. 读取图片数据
            inputStream = Files.newInputStream(imageFile.toPath());
            outputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[8192];
@@ -51,16 +57,12 @@
            byte[] imageBytes = outputStream.toByteArray();
            String base64 = Base64.encodeBase64String(imageBytes);
            // 4. 根据图片类型添加前缀
            String contentType = connection.getContentType();
            if (contentType != null && contentType.startsWith("image/")) {
                return "data:" + contentType + ";base64," + base64;
            }
            return "data:image/jpeg;base64," + base64;
            // 4. 根据文件扩展名添加前缀
            String contentType = getContentType(imagePath);
            return "data:" + contentType + ";base64," + base64;
        } catch (Exception e) {
            log.error("图片转Base64失败,URL:{}", imageUrl, e);
            log.error("本地图片转Base64失败,路径:{}", imagePath, e);
            return null;
        } finally {
            try {
@@ -75,4 +77,30 @@
            }
        }
    }
    /**
     * 根据文件扩展名获取Content-Type
     */
    private static String getContentType(String imagePath) {
        if (StringUtils.isEmpty(imagePath)) {
            return "image/jpeg";
        }
        String ext = imagePath.substring(imagePath.lastIndexOf(".") + 1).toLowerCase();
        switch (ext) {
            case "png":
                return "image/png";
            case "gif":
                return "image/gif";
            case "bmp":
                return "image/bmp";
            case "webp":
                return "image/webp";
            case "svg":
                return "image/svg+xml";
            case "jpg":
            case "jpeg":
            default:
                return "image/jpeg";
        }
    }
}
ruoyi-applet/src/main/resources/application-test.yml
@@ -32,6 +32,12 @@
      # Tomcat启动初始化的线程数,默认值10
      min-spare: 100
# 访客登记同步配置
visitor:
  sync:
    enabled: false  # 是否启用同步
    url: http://对方服务器IP:端口/event-center/security-protection/visitor-register
# 日志配置
logging:
  level:
ruoyi-common/src/main/java/com/ruoyi/common/core/domain/BaseEntity.java
@@ -44,16 +44,16 @@
    private String remark;
    @ApiModelProperty(value = "禁用备注")
    @TableField("disable_remark")
    @TableField(value = "disable_remark" ,exist = false)
    private String disableRemark;
    @ApiModelProperty(value = "操作时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField("operating_time")
    @TableField(value = "operating_time" ,exist = false)
    private LocalDateTime operatingTime;
    @ApiModelProperty(value = "操作人  登录名(登录账号)")
    @TableField("operating_person")
    @TableField(value = "operating_person",exist = false)
    private String operatingPerson;
    public String getDisableRemark() {
@@ -82,6 +82,7 @@
    /** 请求参数 */
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    @TableField(exist = false)
    private Map<String, Object> params;
    public String getSearchValue()
ruoyi-system/src/main/java/com/ruoyi/system/domain/Employee.java
@@ -3,6 +3,7 @@
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.Api;
import lombok.Data;
@@ -10,6 +11,7 @@
 * 员工实体(对应 t_employee 表)
 */
@Data
@TableName("t_employee")
public class Employee {
    private Long employeeId;
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitArea.java
@@ -1,5 +1,6 @@
package com.ruoyi.system.domain;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -11,6 +12,7 @@
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_visit_area")
public class SysVisitArea extends BaseEntity
{
    private static final long serialVersionUID = 1L;
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorMachine.java
@@ -38,4 +38,7 @@
    /** 删除标识,0正常 1删除 */
    private String delFlag;
    /** 百度SDK指纹 */
    private String fingerprint;
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorRecord.java
@@ -1,10 +1,14 @@
package com.ruoyi.system.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.system.domain.vo.AreasList;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.util.List;
/**
 * 访客登记记录 t_visitor_record
@@ -13,6 +17,7 @@
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("t_visitor_record")
public class SysVisitorRecord extends BaseEntity
{
    private static final long serialVersionUID = 1L;
@@ -88,4 +93,9 @@
    /** 删除标识:0正常 1删除 */
    private String delFlag;
    @TableField(exist = false)
    private List<AreasList> areasList;
    @TableField(exist = false)
    private String deviceId;
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysVisitorSignoutTemp.java
@@ -1,6 +1,7 @@
package com.ruoyi.system.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -47,6 +48,7 @@
     * 签离时间
     */
    @TableField("sign_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date signTime;
    /**
@@ -95,12 +97,14 @@
     * 创建时间
     */
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    /**
     * 更新时间
     */
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    /**
ruoyi-system/src/main/java/com/ruoyi/system/domain/api/ApiSysVisitorRecord.java
New file
@@ -0,0 +1,102 @@
package com.ruoyi.system.domain.api;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.system.domain.vo.AreasList;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.util.List;
/**
 * 访客登记记录 t_visitor_record
 *
 * @author ruoyi
 */
@Data
@TableName("t_visitor_record")
public class ApiSysVisitorRecord
{
    private static final long serialVersionUID = 1L;
    /** 访客登记记录主键 */
    private Long recordId;
    /** 登记编号 */
    private String registerNo;
    /** 访客姓名 */
    private String visitorName;
    /** 访客身份证号 */
    private String idCard;
    /** 登记类型:APPOINTMENT预约登记 ONSITE现场登记 */
    private String registerType;
    /** 被访人 */
    private String intervieweeName;
    /** 被访人部门 */
    private String intervieweeDept;
    /** 登记接待人 */
    private String receiverName;
    /** 接待人验证方式 */
    private String receiverVerifyMethod;
    /** 访问区域ID,英文逗号分隔 */
    private String visitAreaIds;
    /** 访问区域名称,英文逗号分隔 */
    private String visitAreaNames;
    /** 车牌号 */
    private String plateNo;
    /** 来访事由 */
    private String visitReason;
    /** 有效期开始时间 */
    private Date validStartTime;
    /** 有效期结束时间 */
    private Date validEndTime;
    /** 登记状态 */
    private String status;
    /** 登记时间 */
    private Date registerTime;
    /** 登记来源 */
    private String registerSource;
    /** 访客人像地址 */
    private String faceImageUrl;
    /** 签离时间 */
    private Date signTime;
    /** 签离接待人 */
    private String signReceiverName;
    /** 签离验证方式 */
    private String signVerifyMethod;
    /** 签离人像地址 */
    private String signImageUrl;
    /** 删除标识:0正常 1删除 */
    private String delFlag;
    @TableField(exist = false)
    private List<AreasList> areasList;
    @TableField(exist = false)
    private String deviceId;
    private String remark;
    private Date createTime;
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/AreasList.java
New file
@@ -0,0 +1,9 @@
package com.ruoyi.system.domain.vo;
import lombok.Data;
@Data
public class AreasList {
    private String areaCode;
    private String areaName;
}
ruoyi-system/src/main/java/com/ruoyi/system/dto/ApiVisitorMachineRequest.java
@@ -17,4 +17,7 @@
    @ApiModelProperty("设备ID")
    @NotNull(message = "未找到该设备")
    private String machineId;
    @ApiModelProperty("百度指纹")
    private String fingerprint;
}
ruoyi-system/src/main/java/com/ruoyi/system/dto/ApiVisitorRecodeSingoutRequest.java
@@ -1,5 +1,6 @@
package com.ruoyi.system.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -16,8 +17,8 @@
@ApiModel(value = "ApiVisitorRecodeSingoutRequest", description = "离线上传的访客身份证签离")
public class ApiVisitorRecodeSingoutRequest
{
    @NotBlank(message = "设备编号不能为空")
    @ApiModelProperty(value = "设备编号")
//    @NotBlank(message = "设备编号不能为空")
    @ApiModelProperty(value = "设备编号,非必填")
    private String deviceNo;
    /**
@@ -42,8 +43,13 @@
    /** 签离时间 */
    @ApiModelProperty(value = "签离时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date signTime;
//    @NotBlank(message = "登记类型不能为空")
//    @ApiModelProperty(value = "登记类型:APPOINTMENT预约登记 ONSITE现场登记")
//    private String registerType;
    /** 签离接待人 */
    @ApiModelProperty(value = "签离接待人")
    private String signReceiverName;
ruoyi-system/src/main/java/com/ruoyi/system/dto/ApiVisitorRecordSinginRequest.java
@@ -17,8 +17,8 @@
@ApiModel(value = "ApiVisitorRecordSinginRequest", description = "离线模式签到数据")
public class ApiVisitorRecordSinginRequest
{
    @NotBlank(message = "设备编号不能为空")
    @ApiModelProperty(value = "设备编号")
//    @NotBlank(message = "设备编号不能为空")
    @ApiModelProperty(value = "设备编号,非必填")
    private String deviceNo;
    /**
@@ -48,6 +48,10 @@
    @ApiModelProperty(value = "接待人验证方式")
    private String receiverVerifyMethod;
    @NotBlank(message = "登记类型不能为空")
    @ApiModelProperty(value = "登记类型:APPOINTMENT预约登记 ONSITE现场登记")
    private String registerType;
    /**
     * 访问区域ID,英文逗号分隔
     */
@@ -70,21 +74,21 @@
     * 有效期开始时间
     */
    @ApiModelProperty(value = "有效期开始时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyyMMddHHmmss")
    private Date validStartTime;
    /**
     * 有效期结束时间
     */
    @ApiModelProperty(value = "有效期结束时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyyMMddHHmmss")
    private Date validEndTime;
    /**
     * 登记时间
     */
    @ApiModelProperty(value = "登记时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyyMMddHHmmss")
    private Date registerTime;
    /**
ruoyi-system/src/main/java/com/ruoyi/system/dto/EmployeeDto.java
@@ -25,4 +25,10 @@
    @ApiModelProperty("人像图片")
    private String faceImage;
    @ApiModelProperty("手机号")
    private String phone;
    // 手机号加密的
    private String contactPhoneCipher;
}
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysVisitorIdCardOutRequest.java
@@ -22,18 +22,18 @@
    @ApiModelProperty(value = "访客身份证号")
    private String idCard;
    /**
     * 被访人
     */
    @NotBlank(message = "被访人不能为空")
    @ApiModelProperty(value = "被访人")
    private String intervieweeName;
    /**
     * 接待人验证方式
     */
    @ApiModelProperty(value = "接待人验证方式")
    private String receiverVerifyMethod;
//    /**
//     * 被访人
//     */
//    @NotBlank(message = "被访人不能为空")
//    @ApiModelProperty(value = "被访人")
//    private String intervieweeName;
//
//    /**
//     * 接待人验证方式
//     */
//    @ApiModelProperty(value = "接待人验证方式")
//    private String receiverVerifyMethod;
    /** 签离接待人 */
    @ApiModelProperty(value = "签离接待人")
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysVisitorRecordAddRequest.java
@@ -20,11 +20,21 @@
public class SysVisitorRecordAddRequest
{
    /**
     * 设备编号
     */
    @ApiModelProperty(value = "设备编号,非必传")
    private String deviceId;
    /**
     * 访客姓名
     */
    @NotBlank(message = "访客姓名不能为空")
    @ApiModelProperty(value = "访客姓名")
    private String visitorName;
    @NotBlank(message = "登记类型不能为空")
    @ApiModelProperty(value = "登记类型:APPOINTMENT预约登记 ONSITE现场登记")
    private String registerType;
    /**
     * 访客身份证号
@@ -40,18 +50,27 @@
    @ApiModelProperty(value = "被访人")
    private String intervieweeName;
    @ApiModelProperty(value = "被访人电话")
    private String beVisitorPhone;
    /**
     * 接待人验证方式
     */
    @ApiModelProperty(value = "接待人验证方式")
    private String receiverVerifyMethod;
    @ApiModelProperty(value = "登记接待人")
    private String receiverName;
    /**
     * 访问区域ID,英文逗号分隔
     */
    @ApiModelProperty(value = "访问区域ID,英文逗号分隔")
    private String visitAreaIds;
    @ApiModelProperty(value = "访问区域名称,英文逗号分隔")
    private String visitAreaNames;
    /**
     * 车牌号
     */
ruoyi-system/src/main/java/com/ruoyi/system/mapper/EmployeeMapper.java
@@ -1,6 +1,7 @@
package com.ruoyi.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.system.domain.Employee;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -8,7 +9,7 @@
import java.util.List;
@Mapper
public interface EmployeeMapper {
public interface EmployeeMapper extends BaseMapper<Employee> {
    /**
     * 按主键查询启用且未删除的接待人。
@@ -19,7 +20,7 @@
    /**
     * 根据员工姓名精准查询启用的员工
     */
    Employee selectEnabledByEmployeeName(@Param("employeeName") String employeeName);
    List<Employee> selectEnabledByEmployeeName(@Param("employeeName") String employeeName);
    List<Employee> selectAllEnabled();
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysVersionUpdateMapper.java
@@ -20,6 +20,8 @@
    public List<SysVersionUpdateVO> pageList(@Param("query") SysVersionUpdatePageRequest query,
                                             @Param("pageInfo") PageInfo<SysVersionUpdateVO> pageInfo);
    public SysVersionUpdateVO selectLatest();
    public SysVersionUpdateVO selectById(Long updateId);
    public int insertVersionUpdate(SysVersionUpdate versionUpdate);
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysVisitAreaMapper.java
@@ -35,5 +35,7 @@
    public int updateChildrenAncestors(@Param("areaId") Long areaId, @Param("oldAncestors") String oldAncestors,
                                       @Param("newAncestors") String newAncestors);
    public int countChildrenByParentIds(@Param("parentIds") List<Long> parentIds);
    public int deleteByIds(@Param("areaIds") List<Long> areaIds, @Param("updateBy") String updateBy);
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysVisitorRecordMapper.java
@@ -21,9 +21,15 @@
@Mapper
public interface SysVisitorRecordMapper extends BaseMapper<SysVisitorRecord>
{
    // 显式覆盖 updateById 方法
    @Override
    int updateById(@Param("et") SysVisitorRecord entity);
    public List<SysVisitorRecordVO> pageList(@Param("query") SysVisitorRecordPageRequest query,
                                             @Param("pageInfo") PageInfo<SysVisitorRecordVO> pageInfo);
    public List<SysVisitorRecordVO> list(@Param("query") SysVisitorRecordPageRequest query);
    public SysVisitorRecordDetailVO selectDetailById(Long recordId);
    public List<SysVisitorPassRecordVO> selectPassRecordsByRecordId(Long recordId);
ruoyi-system/src/main/java/com/ruoyi/system/mapper/api/SysApiVisitorRecordMapper.java
New file
@@ -0,0 +1,27 @@
package com.ruoyi.system.mapper.api;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.common.basic.PageInfo;
import com.ruoyi.system.domain.SysVisitorRecord;
import com.ruoyi.system.domain.api.ApiSysVisitorRecord;
import com.ruoyi.system.dto.SysVisitorRecordPageRequest;
import com.ruoyi.system.vo.SysVisitorPassRecordVO;
import com.ruoyi.system.vo.SysVisitorRecordDetailVO;
import com.ruoyi.system.vo.SysVisitorRecordVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.Date;
import java.util.List;
/**
 * 访客登记记录数据层
 *
 * @author ruoyi
 */
@Repository
public interface SysApiVisitorRecordMapper extends BaseMapper<ApiSysVisitorRecord>
{
}
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysVersionUpdateService.java
@@ -16,6 +16,8 @@
{
    public PageInfo<SysVersionUpdateVO> pageList(SysVersionUpdatePageRequest request);
    public SysVersionUpdateVO latest();
    public SysVersionUpdateVO detail(SysVersionUpdateDetailRequest request);
    public int add(SysVersionUpdateAddRequest request, String username);
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysVisitorMachineService.java
@@ -37,7 +37,7 @@
     * @param deviceId 设备ID
     * @return 影响行数
     */
    int activateDevice(String deviceId);
    int activateDevice(String deviceId, String fingerprint);
    /**
     * 注销设备
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysVisitorRecordService.java
@@ -7,6 +7,8 @@
import com.ruoyi.system.vo.SysVisitorRecordDetailVO;
import com.ruoyi.system.vo.SysVisitorRecordVO;
import java.util.List;
/**
 * 访客登记记录服务
 *
@@ -16,6 +18,8 @@
{
    public PageInfo<SysVisitorRecordVO> pageList(SysVisitorRecordPageRequest request);
    public List<SysVisitorRecordVO> list(SysVisitorRecordPageRequest request);
    public SysVisitorRecordDetailVO detail(SysVisitorRecordDetailRequest request);
    public int refreshVisitorStatus();
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVersionUpdateServiceImpl.java
@@ -39,6 +39,12 @@
    }
    @Override
    public SysVersionUpdateVO latest()
    {
        return versionUpdateMapper.selectLatest();
    }
    @Override
    public SysVersionUpdateVO detail(SysVersionUpdateDetailRequest request)
    {
        if (request == null || request.getUpdateId() == null)
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitAreaServiceImpl.java
@@ -138,6 +138,10 @@
        {
            getRequiredAreaId(areaId);
        }
        if (visitAreaMapper.countChildrenByParentIds(request.getAreaIds()) > 0)
        {
            throw new ServiceException("存在下级区域");
        }
        return visitAreaMapper.deleteByIds(request.getAreaIds(), username);
    }
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitorConfigServiceImpl.java
@@ -44,6 +44,8 @@
        labels.put("plateNo", "车牌号");
        labels.put("visitReason", "来访事由");
        labels.put("visitArea", "访问区域");
        labels.put("receiverName", "接待人");
        labels.put("registerTime", "登记时间");
        PRINT_FIELD_LABELS = Collections.unmodifiableMap(labels);
    }
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitorMachineServiceImpl.java
@@ -153,7 +153,7 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int activateDevice(String deviceId) {
    public int activateDevice(String deviceId, String fingerprint) {
        // 1. 根据deviceId查询设备
        SysVisitorMachine device = visitorMachineMapper.checkDeviceIdUnique(deviceId, null);
        if (device == null || device.getDelFlag().equals("1")) {
@@ -175,7 +175,7 @@
        update.setMachineId(device.getMachineId());
        update.setOnlineStatus("ONLINE");  // 或 "ACTIVATED",根据你的枚举定义
        update.setUpdateTime(new Date());
        update.setFingerprint(fingerprint);
        return visitorMachineMapper.updateMachine(update);
    }
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysVisitorRecordServiceImpl.java
@@ -7,6 +7,8 @@
import com.ruoyi.system.dto.SysVisitorRecordDeleteRequest;
import com.ruoyi.system.dto.SysVisitorRecordDetailRequest;
import com.ruoyi.system.dto.SysVisitorRecordPageRequest;
import com.ruoyi.system.domain.SysVisitArea;
import com.ruoyi.system.mapper.SysVisitAreaMapper;
import com.ruoyi.system.mapper.SysVisitorRecordMapper;
import com.ruoyi.system.service.ISysVisitorRecordService;
import com.ruoyi.system.vo.SysVisitorRecordDetailVO;
@@ -34,24 +36,25 @@
    @Autowired
    private SysVisitorRecordMapper visitorRecordMapper;
    @Autowired
    private SysVisitAreaMapper visitAreaMapper;
    @Override
    public PageInfo<SysVisitorRecordVO> pageList(SysVisitorRecordPageRequest request)
    {
        refreshVisitorStatus();
        SysVisitorRecordPageRequest query = request == null ? new SysVisitorRecordPageRequest() : request;
        query.setIntervieweeName(StringUtils.trim(query.getIntervieweeName()));
        query.setIntervieweeKeyword(StringUtils.trim(query.getIntervieweeKeyword()));
        query.setIntervieweeDept(StringUtils.trim(query.getIntervieweeDept()));
        query.setVisitAreaKeyword(StringUtils.trim(query.getVisitAreaKeyword()));
        query.setStatus(StringUtils.trim(query.getStatus()));
        query.setVisitorName(StringUtils.trim(query.getVisitorName()));
        query.setVisitorKeyword(StringUtils.trim(query.getVisitorKeyword()));
        query.setVisitReason(StringUtils.trim(query.getVisitReason()));
        query.setRegisterType(StringUtils.trim(query.getRegisterType()));
        SysVisitorRecordPageRequest query = normalizeQuery(request);
        PageInfo<SysVisitorRecordVO> pageInfo = new PageInfo<SysVisitorRecordVO>(query.getPageNum(), query.getPageSize());
        List<SysVisitorRecordVO> list = visitorRecordMapper.pageList(query, pageInfo);
        pageInfo.setRecords(list);
        return pageInfo;
    }
    @Override
    public List<SysVisitorRecordVO> list(SysVisitorRecordPageRequest request)
    {
        refreshVisitorStatus();
        return visitorRecordMapper.list(normalizeQuery(request));
    }
    @Override
@@ -96,15 +99,42 @@
    private void fillDetailDisplayFields(SysVisitorRecordDetailVO detail)
    {
        List<String> visitAreaNames = resolveVisitAreaNames(detail);
        detail.setIdCardMask(maskIdCard(detail.getIdCard()));
        detail.setRegisterTypeName(registerTypeName(detail.getRegisterType()));
        detail.setStatusName(statusName(detail.getStatus()));
        detail.setReceiverVerifyMethodName(verifyMethodName(detail.getReceiverVerifyMethod()));
        detail.setSignVerifyMethodName(verifyMethodName(detail.getSignVerifyMethod()));
        detail.setVisitAreaNameList(splitNames(detail.getVisitAreaNames()));
        detail.setVisitAreaNameList(visitAreaNames);
        detail.setVisitAreaNames(String.join(",", visitAreaNames));
        detail.setValidTimeText(validTimeText(detail.getValidStartTime(), detail.getValidEndTime()));
        detail.setRegisterCollectTime(detail.getRegisterTime());
        detail.setSignCollectTime(detail.getSignTime());
    }
    private List<String> resolveVisitAreaNames(SysVisitorRecordDetailVO detail)
    {
        List<String> names = splitNames(detail.getVisitAreaNames());
        if (!names.isEmpty() || StringUtils.isBlank(detail.getVisitAreaIds()))
        {
            return names;
        }
        for (String areaIdText : detail.getVisitAreaIds().split(","))
        {
            try
            {
                SysVisitArea area = visitAreaMapper.selectById(Long.valueOf(StringUtils.trim(areaIdText)));
                if (area != null && StringUtils.isNotBlank(area.getAreaName()))
                {
                    names.add(area.getAreaName());
                }
            }
            catch (NumberFormatException ignored)
            {
                continue;
            }
        }
        return names;
    }
    private String maskIdCard(String idCard)
@@ -259,4 +289,19 @@
        }
        return recordId;
    }
    private SysVisitorRecordPageRequest normalizeQuery(SysVisitorRecordPageRequest request)
    {
        SysVisitorRecordPageRequest query = request == null ? new SysVisitorRecordPageRequest() : request;
        query.setIntervieweeName(StringUtils.trim(query.getIntervieweeName()));
        query.setIntervieweeKeyword(StringUtils.trim(query.getIntervieweeKeyword()));
        query.setIntervieweeDept(StringUtils.trim(query.getIntervieweeDept()));
        query.setVisitAreaKeyword(StringUtils.trim(query.getVisitAreaKeyword()));
        query.setStatus(StringUtils.trim(query.getStatus()));
        query.setVisitorName(StringUtils.trim(query.getVisitorName()));
        query.setVisitorKeyword(StringUtils.trim(query.getVisitorKeyword()));
        query.setVisitReason(StringUtils.trim(query.getVisitReason()));
        query.setRegisterType(StringUtils.trim(query.getRegisterType()));
        return query;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/vo/SysVisitorMachineVO.java
@@ -25,6 +25,8 @@
    private String deviceId;
    private String fingerprint;
    private Boolean enabled;
    private String onlineStatus;
ruoyi-system/src/main/resources/mapper/system/EmployeeMapper.xml
@@ -22,7 +22,7 @@
    <!-- 人脸图片不参与本接口逻辑,且 longtext 体积较大,列表中排除,减少不必要的数据传输 -->
    <sql id="Base_Column_List">
        employee_id, dept_id, employee_name, employee_no, contact_phone_cipher,
        employee_id, dept_id, employee_name, face_image, employee_no, contact_phone_cipher,
        contact_phone_hash, `position`, card_no, enabled, del_flag,
        create_by, create_time, update_by, update_time
    </sql>
@@ -50,7 +50,7 @@
        SELECT
        <include refid="Base_Column_List"/>
        FROM t_employee
        AND enabled = 1
        where enabled = 1
        AND del_flag = '0'
    </select>
ruoyi-system/src/main/resources/mapper/system/SysVersionUpdateMapper.xml
@@ -42,6 +42,13 @@
          and del_flag = '0'
    </select>
    <select id="selectLatest" resultMap="SysVersionUpdateVOResult">
        <include refid="selectVersionUpdateVo" />
        where del_flag = '0'
        order by create_time desc, update_id desc
        limit 1
    </select>
    <insert id="insertVersionUpdate" parameterType="SysVersionUpdate" useGeneratedKeys="true" keyProperty="updateId">
        insert into t_version_update (
            version_no,
ruoyi-system/src/main/resources/mapper/system/SysVisitAreaMapper.xml
@@ -147,6 +147,16 @@
          and find_in_set(#{areaId}, ancestors)
    </update>
    <select id="countChildrenByParentIds" resultType="int">
        select count(1)
        from t_visit_area
        where del_flag = '0'
          and parent_id in
          <foreach collection="parentIds" item="parentId" open="(" separator="," close=")">
              #{parentId}
          </foreach>
    </select>
    <update id="deleteByIds">
        update t_visit_area
        set del_flag = '1',
ruoyi-system/src/main/resources/mapper/system/SysVisitorMachineMapper.xml
@@ -12,6 +12,7 @@
        <result property="deviceId"     column="device_id" />
        <result property="enabled"      column="enabled" />
        <result property="onlineStatus" column="online_status" />
        <result property="fingerprint"  column="fingerprint" />
        <result property="delFlag"      column="del_flag" />
        <result property="remark"       column="remark" />
        <result property="createBy"     column="create_by" />
@@ -27,6 +28,7 @@
        <result property="deviceName"   column="device_name" />
        <result property="brand"        column="brand" />
        <result property="deviceId"     column="device_id" />
        <result property="fingerprint"  column="fingerprint" />
        <result property="enabled"      column="enabled" />
        <result property="onlineStatus" column="online_status" />
        <result property="remark"       column="remark" />
@@ -41,6 +43,7 @@
               device_id,
               enabled,
               online_status,
               fingerprint,
               del_flag,
               remark,
               create_by,
@@ -57,6 +60,7 @@
               m.device_name,
               m.brand,
               m.device_id,
               m.fingerprint,
               m.enabled,
               m.online_status,
               m.remark,
@@ -141,14 +145,35 @@
    <update id="updateMachine" parameterType="SysVisitorMachine">
        update t_visitor_machine
        set area_id = #{areaId},
            device_name = #{deviceName},
            brand = #{brand},
            device_id = #{deviceId},
            enabled = #{enabled},
            remark = #{remark},
        <set>
            <if test="areaId != null and areaId != ''">
                area_id = #{areaId},
            </if>
            <if test="deviceName != null and deviceName != ''">
                device_name = #{deviceName},
            </if>
            <if test="brand != null and brand != ''">
                brand = #{brand},
            </if>
            <if test="deviceId != null and deviceId != ''">
                device_id = #{deviceId},
            </if>
            <if test="enabled != null">
                enabled = #{enabled},    <!-- 注意:enabled 可能是 Boolean 或 Integer,若允许 false 或 0,则只判 null -->
            </if>
            <if test="onlineStatus != null and onlineStatus != ''">
                online_status = #{onlineStatus},
            </if>
            <if test="fingerprint != null and fingerprint != ''">
                fingerprint = #{fingerprint},
            </if>
            <if test="remark != null and remark != ''">
                remark = #{remark},
            </if>
            <!-- 以下两个字段始终更新,不加 if -->
            update_by = #{updateBy},
            update_time = sysdate()
        </set>
        where machine_id = #{machineId}
          and del_flag = '0'
    </update>
ruoyi-system/src/main/resources/mapper/system/SysVisitorRecordMapper.xml
@@ -86,7 +86,7 @@
        from t_visitor_record
    </sql>
    <select id="pageList" resultMap="SysVisitorRecordVOResult">
    <sql id="selectVisitorRecordList">
        <include refid="selectVisitorRecordBase" />
        <where>
            and del_flag = '0'
@@ -124,13 +124,21 @@
                )
            </if>
            <if test="query.visitReason != null and query.visitReason != ''">
                and visit_reason = #{query.visitReason}
                and visit_reason like concat('%', #{query.visitReason}, '%')
            </if>
            <if test="query.registerType != null and query.registerType != ''">
                and register_type = #{query.registerType}
            </if>
        </where>
        order by register_time desc, record_id desc
    </sql>
    <select id="pageList" resultMap="SysVisitorRecordVOResult">
        <include refid="selectVisitorRecordList" />
    </select>
    <select id="list" resultMap="SysVisitorRecordVOResult">
        <include refid="selectVisitorRecordList" />
    </select>
    <select id="selectDetailById" parameterType="Long" resultMap="SysVisitorRecordDetailVOResult">
ruoyi-ui/.env.development
@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 运营活动管理平台
VUE_APP_TITLE = 访客机自助登记管理平台
# 开发环境配置
ENV = 'development'
@@ -9,7 +9,7 @@
# 访客后台前端部署模式:standalone 单机版需要输入IP,cluster 集群版不输入IP
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
# 开发环境通过 /dev-api 代理到本地 9098
ruoyi-ui/.env.production
@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 运营活动管理平台
VUE_APP_TITLE = 访客机自助登记管理平台
# 生产环境配置
ENV = 'production'
ruoyi-ui/src/api/visitor-admin/record.js
@@ -5,11 +5,15 @@
}
export function getVisitorRecord(recordId) {
  return request({ url: '/system/visitor/record/detail', method: 'post', data: { recordId } })
  return request({ url: '/system/visitor/record/detail', method: 'post', data: { recordId }})
}
export function delVisitorRecord(recordIds) {
  return request({ url: '/system/visitor/record/delete', method: 'post', data: { recordIds } })
  return request({ url: '/system/visitor/record/delete', method: 'post', data: { recordIds }})
}
export function exportVisitorRecord(data) {
  return request({ url: '/system/visitor/record/export', method: 'post', data })
}
export function listOperationLog(data) {
@@ -19,4 +23,3 @@
export function exportOperationLog(data) {
  return request({ url: '/system/operation/log/export', method: 'post', data })
}
ruoyi-ui/src/assets/styles/visitor-admin.scss
@@ -92,9 +92,9 @@
      min-width: 0 !important;
      height: 36px !important;
      line-height: 36px !important;
      margin: 0;
      margin: 0 0 4px;
      padding: 0 0 0 48px !important;
      border-radius: 0;
      border-radius: 13px;
      color: rgba(255, 255, 255, 0.92) !important;
      background: transparent !important;
      font-size: 12px;
@@ -128,11 +128,13 @@
      background-color: rgba(255, 255, 255, 0.14) !important;
    }
    .theme-dark .el-submenu .el-menu-item:hover,
    .theme-dark .el-submenu .el-menu-item.is-active,
    .el-submenu .el-menu-item:hover,
    .el-submenu .el-menu-item.is-active {
      color: #fff !important;
      background: transparent !important;
      background-color: transparent !important;
      background: rgba(255, 255, 255, 0.14) !important;
      background-color: rgba(255, 255, 255, 0.14) !important;
    }
    > .el-scrollbar > .el-scrollbar__wrap > .el-scrollbar__view > .el-menu > .el-menu-item.is-active,
@@ -1596,49 +1598,6 @@
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: middle;
}
.content-preview {
  position: absolute;
  left: -16px;
  top: 38px;
  z-index: 10;
  width: 204px;
  min-height: 102px;
  display: none;
  padding: 0 10px 12px;
  border: 1px solid #f0bc2d;
  border-radius: 2px;
  background: #fff;
  box-shadow: 0 4px 12px rgba(15, 35, 70, 0.12);
}
.version-content-cell:hover .content-preview {
  display: block;
}
.preview-index {
  height: 16px;
  margin: 0 -10px 12px;
  padding-left: 9px;
  color: #111827;
  background: #ffd753;
  font-weight: 700;
  line-height: 16px;
}
.preview-content {
  min-height: 42px;
  color: #111827;
  font-weight: 700;
  line-height: 18px;
  word-break: break-word;
}
.preview-foot {
  margin-top: 26px;
  color: #8a9ab0;
  font-size: 11px;
}
.table-actions {
ruoyi-ui/src/components/ImageUpload/index.vue
@@ -68,6 +68,11 @@
    isShowTip: {
      type: Boolean,
      default: true
    },
    // 是否在上传前压缩图片
    compress: {
      type: Boolean,
      default: false
    }
  },
  data() {
@@ -140,15 +145,63 @@
        this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`);
        return false;
      }
      if (this.fileSize) {
        const isLt = file.size / 1024 / 1024 < this.fileSize;
        if (!isLt) {
          this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`);
          return false;
      const compressedFile = this.compress ? this.compressImage(file) : Promise.resolve(file);
      return compressedFile.then(uploadFile => {
        if (this.fileSize) {
          const isLt = uploadFile.size / 1024 / 1024 < this.fileSize;
          if (!isLt) {
            throw new Error("FILE_SIZE_EXCEEDED");
          }
        }
      }
      this.$modal.loading("正在上传图片,请稍候...");
      this.number++;
        this.$modal.loading("正在上传图片,请稍候...");
        this.number++;
        return uploadFile;
      }).catch(error => {
        const message = error && error.message === "FILE_SIZE_EXCEEDED"
          ? `上传头像图片大小不能超过 ${this.fileSize} MB!`
          : "图片压缩失败,请重试";
        this.$modal.msgError(message);
        return Promise.reject(error);
      });
    },
    // 将人脸图片限制在合理尺寸并转为 JPEG,减少上传和设备同步数据量
    compressImage(file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = event => {
          const image = new Image();
          image.onload = () => {
            const maxWidth = 1280;
            const maxHeight = 1280;
            const scale = Math.min(1, maxWidth / image.width, maxHeight / image.height);
            const resized = scale < 1;
            const canvas = document.createElement("canvas");
            canvas.width = Math.max(1, Math.round(image.width * scale));
            canvas.height = Math.max(1, Math.round(image.height * scale));
            const context = canvas.getContext("2d");
            if (!context) {
              resolve(file);
              return;
            }
            context.drawImage(image, 0, 0, canvas.width, canvas.height);
            canvas.toBlob(blob => {
              if (!blob || (!resized && blob.size >= file.size)) {
                resolve(file);
                return;
              }
              const fileName = file.name.replace(/\.[^.]+$/, "") + ".jpg";
              resolve(new File([blob], fileName, {
                type: "image/jpeg",
                lastModified: Date.now()
              }));
            }, "image/jpeg", 0.82);
          };
          image.onerror = reject;
          image.src = event.target.result;
        };
        reader.onerror = reject;
        reader.readAsDataURL(file);
      });
    },
    // 文件个数超出
    handleExceed() {
ruoyi-ui/src/layout/components/Navbar.vue
@@ -120,7 +120,7 @@
      },
      passwordRules: {
        oldPassword: [{ required: true, message: '请输入原密码', trigger: 'blur' }],
        newPassword: [{ validator: strongPassword, trigger: 'blur' }],
        newPassword: [{ required: true, validator: strongPassword, trigger: 'blur' }],
        confirmPassword: [{ required: true, message: '请确认新密码', trigger: 'blur' }, { validator: confirmPassword, trigger: 'blur' }]
      }
    }
ruoyi-ui/src/utils/request.js
@@ -4,7 +4,6 @@
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from "@/utils/ruoyi";
import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
import visitorAdminConfig from '@/config/visitor-admin'
@@ -84,8 +83,6 @@
  config.baseURL = getVisitorApiBaseUrl()
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  // 是否需要防止数据重复提交
  const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
  if (getToken() && !isToken) {
    config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  }
@@ -95,35 +92,6 @@
    url = url.slice(0, -1);
    config.params = {};
    config.url = url;
  }
  if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
    const requestObj = {
      url: config.url,
      data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
      time: new Date().getTime()
    }
    const requestSize = Object.keys(JSON.stringify(requestObj)).length; // 请求数据大小
    const limitSize = 5 * 1024 * 1024; // 限制存放数据5M
    if (requestSize >= limitSize) {
      console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。')
      return config;
    }
    const sessionObj = cache.session.getJSON('sessionObj')
    if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
      cache.session.setJSON('sessionObj', requestObj)
    } else {
      const s_url = sessionObj.url;                  // 请求地址
      const s_data = sessionObj.data;                // 请求数据
      const s_time = sessionObj.time;                // 请求时间
      const interval = 1000;                         // 间隔时间(ms),小于此时间视为重复提交
      if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
        const message = '数据正在处理,请勿重复提交';
        console.warn(`[${s_url}]: ` + message)
        return Promise.reject(new Error(message))
      } else {
        cache.session.setJSON('sessionObj', requestObj)
      }
    }
  }
  return config
}, error => {
ruoyi-ui/src/views/visitor-admin/device/index.vue
@@ -60,6 +60,7 @@
            <el-table-column label="设备名称" prop="deviceName" min-width="120" show-overflow-tooltip />
            <el-table-column label="品牌" prop="brand" min-width="100" show-overflow-tooltip />
            <el-table-column label="设备ID" prop="deviceId" min-width="150" show-overflow-tooltip />
            <el-table-column v-if="deviceType === 'machine'" label="百度SDK指纹" prop="fingerprint" min-width="160" show-overflow-tooltip />
            <el-table-column label="所属区域" prop="areaName" min-width="130" show-overflow-tooltip />
            <el-table-column label="启用状态" align="center" width="100">
              <template slot-scope="scope">
ruoyi-ui/src/views/visitor-admin/organization/employee.vue
@@ -142,8 +142,8 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="人脸图片" prop="faceImage">
          <el-input v-model="form.faceImage" placeholder="请输入人脸图片地址" />
        <el-form-item label="人脸图片" prop="faceImage" class="employee-face-upload">
          <image-upload v-model="form.faceImage" :limit="1" :file-size="5" :is-show-tip="false" :compress="true" />
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
@@ -314,7 +314,8 @@
      this.title = '新增员工'
      this.form = {
        deptId: this.queryParams.deptId,
        enabled: true
        enabled: true,
        faceImage: ''
      }
      this.open = true
    },
@@ -423,4 +424,15 @@
.employee-add-col .el-button {
  border-radius: 8px;
}
.employee-face-upload ::v-deep .el-upload--picture-card,
.employee-face-upload ::v-deep .el-upload-list--picture-card .el-upload-list__item {
  width: 96px;
  height: 96px;
  line-height: 96px;
}
.employee-face-upload ::v-deep .el-upload--picture-card {
  border-radius: 8px;
}
</style>
ruoyi-ui/src/views/visitor-admin/record/detail.vue
@@ -125,7 +125,9 @@
  computed: {
    // 目的:把详情访问区域拆成标签数组,适配中英文逗号和数组格式。
    visitAreaTags() {
      const value = this.detail.visitAreaNames
      const value = this.detail.visitAreaNameList && this.detail.visitAreaNameList.length
        ? this.detail.visitAreaNameList
        : this.detail.visitAreaNames
      if (Array.isArray(value)) {
        return value.map(item => String(item || '').trim()).filter(Boolean)
      }
ruoyi-ui/src/views/visitor-admin/record/visitor.vue
@@ -4,7 +4,7 @@
      <header class="visitor-header">
        <h1>访客登记记录</h1>
      </header>
    <el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" class="visitor-query">
    <el-form ref="queryForm" :model="queryParams" size="small" :inline="true" class="visitor-query">
      <el-form-item label="访客姓名" prop="visitorName">
        <el-input v-model="queryParams.visitorName" placeholder="请输入访客姓名" clearable @keyup.enter.native="handleQuery" />
      </el-form-item>
@@ -13,6 +13,9 @@
      </el-form-item>
      <el-form-item label="被访部门" prop="intervieweeDept">
        <el-input v-model="queryParams.intervieweeDept" placeholder="请输入被访部门" clearable @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="来访事由" prop="visitReason">
        <el-input v-model="queryParams.visitReason" placeholder="请输入来访事由" clearable @keyup.enter.native="handleQuery" />
      </el-form-item>
      <el-form-item label="访问区域" prop="areaId">
        <treeselect v-model="queryParams.areaId" :options="areaOptions" placeholder="请选择区域" clearable class="query-tree" />
@@ -44,16 +47,21 @@
    </el-form>
    <el-row :gutter="10" class="mb8">
      <el-col v-if="!isMiddlePlatformReadonly" v-hasPermi="['visitor:record:remove']" :span="1.5">
        <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete()">删除</el-button>
      <el-col v-hasPermi="['visitor:record:export']" :span="1.5">
        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
      </el-col>
      <right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
      <el-col v-if="!isMiddlePlatformReadonly" v-hasPermi="['visitor:record:remove']" :span="1.5">
        <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">批量删除</el-button>
      </el-col>
    </el-row>
    <el-table v-loading="loading" class="visitor-table" :data="recordList" @selection-change="handleSelectionChange">
      <el-table-column v-if="!isMiddlePlatformReadonly" type="selection" width="50" align="center" />
      <el-table-column label="登记编号" prop="registerNo" min-width="150" show-overflow-tooltip />
      <el-table-column label="访客姓名" prop="visitorName" min-width="150" />
      <el-table-column label="身份证" min-width="150" show-overflow-tooltip>
        <template slot-scope="scope">{{ maskIdCard(scope.row.idCard) }}</template>
      </el-table-column>
      <el-table-column label="登记类型" align="center" min-width="150">
        <template slot-scope="scope">{{ optionLabel(registerTypes, scope.row.registerType) }}</template>
      </el-table-column>
@@ -61,6 +69,10 @@
      <el-table-column label="被访部门" prop="intervieweeDept" min-width="150" show-overflow-tooltip />
      <el-table-column label="访问区域" prop="visitAreaNames" min-width="150" show-overflow-tooltip />
      <el-table-column label="车牌号" prop="plateNo" min-width="150" />
      <el-table-column label="来访事由" prop="visitReason" min-width="150" show-overflow-tooltip />
      <el-table-column label="有效期" min-width="260" show-overflow-tooltip>
        <template slot-scope="scope">{{ validTimeText(scope.row.validStartTime, scope.row.validEndTime) }}</template>
      </el-table-column>
      <el-table-column label="状态" align="center" min-width="150">
        <template slot-scope="scope">
          <span class="record-status-pill" :class="statusClass(scope.row.status)">{{ optionLabel(statusOptions, scope.row.status) }}</span>
@@ -69,10 +81,9 @@
      <el-table-column label="登记时间" align="center" prop="registerTime" min-width="150">
        <template slot-scope="scope">{{ parseTime(scope.row.registerTime) }}</template>
      </el-table-column>
      <el-table-column label="操作" align="center" width="150" fixed="right" class-name="small-padding fixed-width">
      <el-table-column label="操作" align="center" width="90" fixed="right" class-name="small-padding fixed-width">
        <template slot-scope="scope">
          <el-button size="mini" type="text" icon="el-icon-view" @click="handleDetail(scope.row)">详情</el-button>
          <el-button v-if="!isMiddlePlatformReadonly" v-hasPermi="['visitor:record:remove']" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
@@ -83,10 +94,11 @@
</template>
<script>
import { saveAs } from 'file-saver'
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
import { treeArea } from '@/api/visitor-admin/area'
import { listVisitorRecord, delVisitorRecord } from '@/api/visitor-admin/record'
import { listVisitorRecord, delVisitorRecord, exportVisitorRecord } from '@/api/visitor-admin/record'
import { getResponseData, getPageRows, getPageTotal, normalizeAreaOptions, toIdArray } from '@/utils/visitor-admin'
export default {
@@ -96,7 +108,6 @@
  data() {
    return {
      loading: false,
      showSearch: true,
      multiple: true,
      ids: [],
      total: 0,
@@ -197,6 +208,24 @@
      }
      return 'is-neutral'
    },
    // 目的:列表只展示身份证前四位和后四位,中间内容统一脱敏。
    maskIdCard(value) {
      const text = String(value || '')
      if (!text) {
        return '-'
      }
      if (text.length <= 8) {
        return text
      }
      return text.slice(0, 4) + '*'.repeat(text.length - 8) + text.slice(-4)
    },
    // 目的:把有效期开始和结束时间合并成列表展示文本。
    validTimeText(start, end) {
      if (!start && !end) {
        return '-'
      }
      return (this.parseTime(start) || '-') + ' 至 ' + (this.parseTime(end) || '-')
    },
    // 目的:从第一页重新执行访客记录查询。
    handleQuery() {
      this.queryParams.pageNum = 1
@@ -208,6 +237,66 @@
      this.resetForm('queryForm')
      this.handleQuery()
    },
    // 目的:导出当前筛选条件下的访客登记记录。
    handleExport() {
      exportVisitorRecord(this.buildQuery()).then(res => {
        const rows = Array.isArray(getResponseData(res)) ? getResponseData(res) : []
        const blob = new Blob([this.buildExcelXml(rows)], { type: 'application/vnd.ms-excel;charset=utf-8' })
        saveAs(blob, `访客登记记录_${this.parseTime(new Date(), '{y}{m}{d}{h}{i}{s}')}.xls`)
      })
    },
    // 目的:把访客登记记录转换成 Excel 2003 XML,保持与操作日志导出方式一致。
    buildExcelXml(rows) {
      const columns = [
        { title: '登记编号', key: 'registerNo', width: 150 },
        { title: '访客姓名', key: 'visitorName', width: 100 },
        { title: '证件号码', key: 'idCard', width: 170 },
        { title: '登记类型', value: row => this.optionLabel(this.registerTypes, row.registerType), width: 90 },
        { title: '被访人', key: 'intervieweeName', width: 100 },
        { title: '被访部门', key: 'intervieweeDept', width: 130 },
        { title: '访问区域', key: 'visitAreaNames', width: 150 },
        { title: '车牌号', key: 'plateNo', width: 100 },
        { title: '来访事由', key: 'visitReason', width: 180 },
        { title: '有效期开始时间', value: row => this.parseTime(row.validStartTime) || '', width: 150 },
        { title: '有效期结束时间', value: row => this.parseTime(row.validEndTime) || '', width: 150 },
        { title: '状态', value: row => this.optionLabel(this.statusOptions, row.status), width: 100 },
        { title: '登记时间', value: row => this.parseTime(row.registerTime) || '', width: 150 }
      ]
      const columnXml = columns.map(column => `<Column ss:Width="${column.width}"/>`).join('')
      const headerXml = `<Row>${columns.map(column => this.excelCell(column.title, 'Header')).join('')}</Row>`
      const bodyXml = rows.map(row => `<Row>${columns.map(column => {
        const value = column.value ? column.value(row) : row[column.key]
        return this.excelCell(value)
      }).join('')}</Row>`).join('')
      return `<?xml version="1.0" encoding="UTF-8"?>
<?mso-application progid="Excel.Sheet"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
 xmlns:o="urn:schemas-microsoft-com:office:office"
 xmlns:x="urn:schemas-microsoft-com:office:excel"
 xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
 <Styles>
  <Style ss:ID="Header"><Font ss:Bold="1"/><Interior ss:Color="#EAF1F9" ss:Pattern="Solid"/></Style>
 </Styles>
 <Worksheet ss:Name="访客登记记录">
  <Table>${columnXml}${headerXml}${bodyXml}</Table>
 </Worksheet>
</Workbook>`
    },
    // 目的:生成 Excel 文本单元格并按需应用表头样式。
    excelCell(value, styleId) {
      const style = styleId ? ` ss:StyleID="${styleId}"` : ''
      return `<Cell${style}><Data ss:Type="String">${this.escapeXml(value)}</Data></Cell>`
    },
    // 目的:转义 Excel XML 特殊字符,避免访客数据破坏文件结构。
    escapeXml(value) {
      return String(value == null ? '' : value).replace(/[<>&'"]/g, char => ({
        '<': '&lt;',
        '>': '&gt;',
        '&': '&amp;',
        "'": '&apos;',
        '"': '&quot;'
      }[char]))
    },
    // 目的:同步表格勾选状态,控制批量删除按钮可用性。
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.recordId)
@@ -217,14 +306,14 @@
    handleDetail(row) {
      this.$router.push(`/visitor/record/detail/${row.recordId}`)
    },
    // 目的:删除访客记录;中台只读模式下禁止本地删除。
    handleDelete(row) {
    // 目的:批量删除已勾选的访客记录;中台只读模式下禁止本地删除。
    handleDelete() {
      if (this.isMiddlePlatformReadonly) {
        this.denyMiddlePlatformWrite()
        return
      }
      const ids = row ? [row.recordId] : toIdArray(this.ids)
      this.$modal.confirm('是否确认删除选中的访客登记记录?').then(() => delVisitorRecord(ids)).then(() => {
      const ids = toIdArray(this.ids)
      this.$modal.confirm(`是否确认批量删除选中的 ${ids.length} 条访客登记记录?`).then(() => delVisitorRecord(ids)).then(() => {
        this.$modal.msgSuccess('删除成功')
        this.getList()
      })
ruoyi-ui/src/views/visitor-admin/system/config.vue
@@ -211,7 +211,9 @@
        { fieldCode: 'visitAreaNames', fieldName: '访问区域' },
        { fieldCode: 'validTime', fieldName: '有效期' },
        { fieldCode: 'plateNo', fieldName: '车牌号' },
        { fieldCode: 'visitReason', fieldName: '来访事由' }
        { fieldCode: 'visitReason', fieldName: '来访事由' },
        { fieldCode: 'receiverName', fieldName: '接待人' },
        { fieldCode: 'registerTime', fieldName: '登记时间' }
      ]
    }
  },
@@ -225,7 +227,9 @@
        visitAreaNames: 'A区 / 办公楼',
        validTime: '2026-07-07 09:00~18:00',
        plateNo: '苏A12345',
        visitReason: '商务拜访'
        visitReason: '商务拜访',
        receiverName: '王五',
        registerTime: '2026-07-23 09:00'
      }
      return this.printFieldOptions
        .filter(item => this.form.printFields.indexOf(item.fieldCode) !== -1)
ruoyi-ui/src/views/visitor-admin/system/version.vue
@@ -22,11 +22,6 @@
            <td>v{{ row.versionNo }}</td>
            <td class="version-content-cell">
              <span class="version-content-text">{{ row.updateContent || '-' }}</span>
              <div class="content-preview">
                <div class="preview-index">{{ index + 1 }}</div>
                <div class="preview-content">{{ row.updateContent || '暂无更新内容' }}</div>
                <div class="preview-foot">{{ parseTime(row.createTime) || '-' }}</div>
              </div>
            </td>
            <td>{{ parseTime(row.createTime) }}</td>
            <td v-if="!isMiddlePlatformReadonly && $auth.hasPermi('visitor:version:remove')">
ruoyi-ui/vue.config.js
@@ -36,14 +36,14 @@
    proxy: {
      // detail: https://cli.vuejs.org/config/#devserver-proxy
      [process.env.VUE_APP_BASE_API]: {
        target: `http://localhost:9098`,
        target: `http://221.182.45.100:9799`,
        changeOrigin: true,
        pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: ''
        }
      },
      '/profile': {
        target: `http://localhost:9098`,
        target: `http://221.182.45.100:9799`,
        changeOrigin: true
      }
    },
visitor-admin-ui/css/version-update.css
@@ -110,70 +110,6 @@
  vertical-align: middle;
}
.content-preview {
  position: absolute;
  left: -62px;
  top: 38px;
  z-index: 10;
  width: 204px;
  min-height: 102px;
  display: none;
  padding: 0 10px 12px;
  border: 1px solid #f0bc2d;
  border-radius: 2px;
  background: #fff;
  box-shadow: 0 4px 12px rgba(15, 35, 70, 0.12);
}
.content-preview::before {
  content: "";
  position: absolute;
  left: 46px;
  top: -188px;
  width: 1px;
  height: 188px;
  background: #dca51f;
}
.content-preview::after {
  content: "";
  position: absolute;
  left: 42px;
  top: -191px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #dca51f;
}
.version-content-cell:hover .content-preview {
  display: block;
}
.preview-index {
  height: 16px;
  margin: 0 -10px 12px;
  padding-left: 9px;
  color: #111827;
  background: #ffd753;
  font-weight: 700;
  line-height: 16px;
}
.preview-content {
  min-height: 42px;
  color: #111827;
  font-weight: 700;
  line-height: 18px;
  word-break: break-word;
}
.preview-foot {
  margin-top: 26px;
  color: #8a9ab0;
  font-size: 11px;
}
.table-actions {
  display: flex;
  align-items: center;
visitor-admin-ui/css/visitor-machine.css
@@ -1020,6 +1020,8 @@
.machine-table th:nth-child(8),
.machine-table td:nth-child(8),
.machine-table th:nth-child(9),
.machine-table td:nth-child(9) {
  width: 11.111%;
.machine-table td:nth-child(9),
.machine-table th:nth-child(10),
.machine-table td:nth-child(10) {
  width: 10%;
}
visitor-admin-ui/js/role-form.js
@@ -89,7 +89,7 @@
    } else if (state.mode === "detail") {
      title = "角色详情";
    }
    document.title = title + " - 运营活动管理平台";
    document.title = title + " - 访客机自助登记管理平台";
    document.getElementById("pageTitle").textContent = title;
    document.getElementById("pageTab").textContent = title;
    document.getElementById("formActions").hidden = state.mode === "detail";
visitor-admin-ui/js/version-update.js
@@ -155,11 +155,6 @@
        '  <td>' + escapeHtml(item.versionNo || "-") + '</td>',
        '  <td class="version-content-cell">',
        '    <span class="version-content-text">' + escapeHtml(item.updateContent || "-") + '</span>',
        '    <div class="content-preview">',
        '      <div class="preview-index">' + rowIndex + '</div>',
        '      <div class="preview-content">' + escapeHtml(item.updateContent || "-") + '</div>',
        '      <div class="preview-foot">' + escapeHtml(item.packageName || "安装包") + ' ' + escapeHtml(shortDate(item.createTime)) + '</div>',
        '    </div>',
        '  </td>',
        '  <td>' + escapeHtml(item.createTime || "-") + '</td>',
        '  <td><div class="table-actions">',
visitor-admin-ui/js/visitor-machine.js
@@ -232,6 +232,7 @@
        '  <td>' + escapeHtml(item.deviceName || "-") + '</td>',
        '  <td>' + escapeHtml(item.brand || "-") + '</td>',
        '  <td>' + escapeHtml(item.deviceId || "-") + '</td>',
        '  <td title="' + escapeHtml(item.fingerprint || "") + '">' + escapeHtml(item.fingerprint || "-") + '</td>',
        '  <td>' + escapeHtml(item.areaName || "-") + '</td>',
        '  <td>' + renderEnabledStatus(item.enabled) + '</td>',
        '  <td>' + renderOnlineStatus(item.onlineStatus) + '</td>',
visitor-admin-ui/js/visitor-record.js
@@ -476,7 +476,15 @@
    if (text.length < 8) {
      return text || "-";
    }
    return text.slice(0, 6) + "********" + text.slice(-4);
    return text.slice(0, 4) + repeatString("*", text.length - 8) + text.slice(-4);
  }
  function repeatString(value, count) {
    var result = "";
    for (var index = 0; index < count; index++) {
      result += value;
    }
    return result;
  }
  function toServerDateTime(value) {
visitor-admin-ui/pages/dashboard.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>首页 - 运营活动管理平台</title>
  <title>首页 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
</head>
@@ -12,7 +12,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
visitor-admin-ui/pages/department-management.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>部门管理 - 运营活动管理平台</title>
  <title>部门管理 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/department-management.css">
@@ -13,7 +13,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
visitor-admin-ui/pages/door-management.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>门禁设备管理 - 运营活动管理平台</title>
  <title>门禁设备管理 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/door-management.css">
@@ -13,7 +13,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
visitor-admin-ui/pages/employee-management.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>员工管理 - 运营活动管理平台</title>
  <title>员工管理 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/employee-management.css">
@@ -13,7 +13,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
visitor-admin-ui/pages/platform-admin.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>平台管理员 - 运营活动管理平台</title>
  <title>平台管理员 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/platform-admin.css">
@@ -13,7 +13,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
        <a class="menu-item" href="./dashboard.html"><span class="menu-icon home"></span><span>首页</span></a>
visitor-admin-ui/pages/protocol-config.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>入园承诺 - 运营活动管理平台</title>
  <title>入园承诺 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/protocol-config.css">
@@ -13,7 +13,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
visitor-admin-ui/pages/role-form.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>角色 - 运营活动管理平台</title>
  <title>角色 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/role-management.css">
@@ -13,7 +13,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
        <a class="menu-item" href="./dashboard.html"><span class="menu-icon home"></span><span>首页</span></a>
visitor-admin-ui/pages/role-management.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>角色管理 - 运营活动管理平台</title>
  <title>角色管理 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/role-management.css">
@@ -13,7 +13,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
        <a class="menu-item" href="./dashboard.html"><span class="menu-icon home"></span><span>首页</span></a>
visitor-admin-ui/pages/system-config.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>信息配置 - 运营活动管理平台</title>
  <title>信息配置 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/system-config.css">
@@ -13,7 +13,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
visitor-admin-ui/pages/version-update.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>访客机版本更新 - 运营活动管理平台</title>
  <title>访客机版本更新 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/version-update.css">
@@ -13,7 +13,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
visitor-admin-ui/pages/visitor-machine.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>访客机管理 - 运营活动管理平台</title>
  <title>访客机管理 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/visitor-machine.css">
@@ -13,7 +13,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
@@ -154,6 +154,7 @@
                    <th>设备名称</th>
                    <th>品牌</th>
                    <th>设备ID</th>
                    <th>百度SDK指纹</th>
                    <th>所属区域</th>
                    <th>启用状态</th>
                    <th>在线状态</th>
visitor-admin-ui/pages/visitor-record-detail.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>预约详情 - 运营活动管理平台</title>
  <title>预约详情 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/visitor-record.css">
@@ -11,7 +11,7 @@
<body>
  <div class="app-shell">
    <aside class="sidebar">
      <div class="brand"><img src="../assets/login-avatar.svg" alt=""><strong>运营活动管理平台</strong></div>
      <div class="brand"><img src="../assets/login-avatar.svg" alt=""><strong>访客机自助登记管理平台</strong></div>
      <nav class="menu" aria-label="主菜单">
        <a class="menu-item" href="./dashboard.html"><span class="menu-icon home"></span><span>首页</span></a>
        <section class="menu-group open">
visitor-admin-ui/pages/visitor-record.html
@@ -3,7 +3,7 @@
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>访客登记记录 - 运营活动管理平台</title>
  <title>访客登记记录 - 访客机自助登记管理平台</title>
  <link rel="stylesheet" href="../css/dashboard.css">
  <link rel="stylesheet" href="../css/components.css">
  <link rel="stylesheet" href="../css/visitor-record.css">
@@ -13,7 +13,7 @@
    <aside class="sidebar">
      <div class="brand">
        <img src="../assets/login-avatar.svg" alt="">
        <strong>运营活动管理平台</strong>
        <strong>访客机自助登记管理平台</strong>
      </div>
      <nav class="menu" aria-label="主菜单">
        <a class="menu-item" href="./dashboard.html"><span class="menu-icon home"></span><span>首页</span></a>