From 20051550f01227ecebbe0bf064e188dd242f58f2 Mon Sep 17 00:00:00 2001
From: liujie <1793218484@qq.com>
Date: 星期一, 21 九月 2026 11:55:02 +0800
Subject: [PATCH] update

---
 ruoyi-admin/src/main/resources/application-test.yml                                  |    2 
 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysCardMapper.java                |   41 +
 ruoyi-ui/src/views/visitor-admin/card/index.vue                                      |  249 ++++++++++
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysEmployeeServiceImpl.java |   65 ++
 ruoyi-system/src/main/resources/mapper/system/SysCardMapper.xml                      |  176 +++++++
 ruoyi-ui/dist.zip                                                                    |    0 
 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCard.java                      |   23 
 ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDetailRequest.java            |   14 
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysCardServiceImpl.java     |  340 ++++++++++++++
 ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardSaveRequest.java              |   23 
 ruoyi-ui/.env.production                                                             |    2 
 doc/sql/20260918_visitor_card_menu.sql                                               |   44 +
 ruoyi-system/src/main/resources/mapper/system/SysEmployeeMapper.xml                  |   16 
 ruoyi-ui/src/router/index.js                                                         |    6 
 doc/sql/20260918_t_card.sql                                                          |   27 +
 ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardEmployeeRequest.java          |   14 
 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysCardService.java             |   32 +
 ruoyi-ui/src/api/visitor-admin/card.js                                               |   69 ++
 ruoyi-admin/src/main/resources/application.yml                                       |    2 
 ruoyi-system/src/main/java/com/ruoyi/system/vo/SysCardVO.java                        |   32 +
 ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysCardController.java        |  186 +++++++
 ruoyi-admin/src/main/resources/application-prod.yml                                  |    2 
 ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDeleteRequest.java            |   16 
 ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardPageRequest.java              |   23 
 ruoyi-system/src/main/resources/mapper/system/SysVisitorConfigMapper.xml             |    3 
 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysEmployeeMapper.java            |    5 
 26 files changed, 1,403 insertions(+), 9 deletions(-)

diff --git a/doc/sql/20260918_t_card.sql b/doc/sql/20260918_t_card.sql
new file mode 100644
index 0000000..b6596eb
--- /dev/null
+++ b/doc/sql/20260918_t_card.sql
@@ -0,0 +1,27 @@
+-- 卡片管理:卡片独立保存,绑定关系同步到 t_employee.card_no。
+CREATE TABLE IF NOT EXISTS `t_card` (
+  `card_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '卡片主键',
+  `card_no` varchar(64) NOT NULL COMMENT '卡号',
+  `employee_id` bigint(20) DEFAULT NULL COMMENT '绑定员工主键',
+  `bind_time` datetime DEFAULT NULL COMMENT '绑定时间',
+  `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '删除标识:0正常 1删除',
+  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  PRIMARY KEY (`card_id`),
+  UNIQUE KEY `uk_card_no` (`card_no`, `del_flag`),
+  KEY `idx_card_employee` (`employee_id`, `del_flag`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='卡片管理表';
+
+-- 兼容已有员工卡号,首次执行迁移时自动建立卡片记录。
+INSERT INTO `t_card` (`card_no`, `employee_id`, `bind_time`, `del_flag`, `create_by`, `create_time`)
+SELECT e.`card_no`, e.`employee_id`, e.`update_time`, '0', 'system', NOW()
+FROM `t_employee` e
+WHERE e.`card_no` IS NOT NULL
+  AND e.`card_no` <> ''
+  AND e.`del_flag` = '0'
+  AND NOT EXISTS (
+    SELECT 1 FROM `t_card` c
+    WHERE c.`card_no` = e.`card_no` AND c.`del_flag` = '0'
+  );
diff --git a/doc/sql/20260918_visitor_card_menu.sql b/doc/sql/20260918_visitor_card_menu.sql
new file mode 100644
index 0000000..b438859
--- /dev/null
+++ b/doc/sql/20260918_visitor_card_menu.sql
@@ -0,0 +1,44 @@
+-- 卡片管理菜单及权限,独立于卡片表结构迁移,且不受中台版本控制。
+INSERT INTO `sys_menu` (
+  `menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`,
+  `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`,
+  `create_by`, `create_time`, `remark`
+) VALUES
+  (3013, '卡片管理', 3010, 3, 'card', 'visitor-admin/card/index', NULL, '1', '1', 'C', '0', '0', 'visitor:card:view', 'bank-card', 'admin', NOW(), '访客后台菜单'),
+  (3100, '卡片新增', 3013, 1, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:card:add', '#', 'admin', NOW(), '访客后台按钮权限'),
+  (3101, '卡片编辑', 3013, 2, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:card:edit', '#', 'admin', NOW(), '访客后台按钮权限'),
+  (3102, '卡片删除', 3013, 3, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:card:remove', '#', 'admin', NOW(), '访客后台按钮权限'),
+  (3103, '卡片导入', 3013, 4, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:card:import', '#', 'admin', NOW(), '访客后台按钮权限'),
+  (3104, '卡片导出', 3013, 5, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:card:export', '#', 'admin', NOW(), '访客后台按钮权限'),
+  (3105, '卡片绑定', 3013, 6, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:card:bind', '#', 'admin', NOW(), '访客后台按钮权限'),
+  (3106, '卡片解绑', 3013, 7, '#', NULL, NULL, '1', '1', 'F', '0', '0', 'visitor:card:unbind', '#', 'admin', NOW(), '访客后台按钮权限')
+ON DUPLICATE KEY UPDATE
+  `menu_name` = VALUES(`menu_name`),
+  `parent_id` = VALUES(`parent_id`),
+  `order_num` = VALUES(`order_num`),
+  `path` = VALUES(`path`),
+  `component` = VALUES(`component`),
+  `query` = VALUES(`query`),
+  `is_frame` = VALUES(`is_frame`),
+  `is_cache` = VALUES(`is_cache`),
+  `menu_type` = VALUES(`menu_type`),
+  `visible` = VALUES(`visible`),
+  `status` = VALUES(`status`),
+  `perms` = VALUES(`perms`),
+  `icon` = VALUES(`icon`),
+  `remark` = VALUES(`remark`),
+  `update_by` = 'admin',
+  `update_time` = NOW();
+
+-- 默认授权给若依管理员角色,普通角色可在角色管理中按需勾选。
+INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
+SELECT r.`role_id`, m.`menu_id`
+FROM `sys_role` r
+JOIN `sys_menu` m ON m.`menu_id` IN (3013, 3100, 3101, 3102, 3103, 3104, 3105, 3106)
+WHERE r.`role_id` = 1
+  AND NOT EXISTS (
+    SELECT 1
+    FROM `sys_role_menu` rm
+    WHERE rm.`role_id` = r.`role_id`
+      AND rm.`menu_id` = m.`menu_id`
+  );
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysCardController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysCardController.java
new file mode 100644
index 0000000..bee3173
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysCardController.java
@@ -0,0 +1,186 @@
+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.SysCardDeleteRequest;
+import com.ruoyi.system.dto.SysCardDetailRequest;
+import com.ruoyi.system.dto.SysCardEmployeeRequest;
+import com.ruoyi.system.dto.SysCardPageRequest;
+import com.ruoyi.system.dto.SysCardSaveRequest;
+import com.ruoyi.system.service.ISysCardService;
+import com.ruoyi.system.vo.SysCardVO;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+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.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.List;
+
+/**
+ * 卡片管理接口。卡片属于本地设备侧数据,不受中台版本管控。
+ */
+@RestController
+@RequestMapping("/system/card")
+public class SysCardController extends BaseController
+{
+    @Autowired
+    private ISysCardService cardService;
+
+    @Log(title = "卡片管理", businessType = BusinessType.SELECT)
+    @PreAuthorize("@ss.hasPermi('visitor:card:view')")
+    @PostMapping("/page")
+    public AjaxResult page(@RequestBody(required = false) SysCardPageRequest request)
+    {
+        return success(cardService.pageList(request));
+    }
+
+    @Log(title = "卡片管理", businessType = BusinessType.SELECT)
+    @PreAuthorize("@ss.hasPermi('visitor:card:view')")
+    @PostMapping("/detail")
+    public AjaxResult detail(@RequestBody SysCardDetailRequest request)
+    {
+        return success(cardService.detail(request));
+    }
+
+    @Log(title = "卡片管理", businessType = BusinessType.SELECT)
+    @PreAuthorize("@ss.hasPermi('visitor:card:view')")
+    @PostMapping("/employee")
+    public AjaxResult employee(@RequestBody SysCardEmployeeRequest request)
+    {
+        return success(cardService.findEmployee(request));
+    }
+
+    @Log(title = "卡片管理", businessType = BusinessType.INSERT)
+    @PreAuthorize("@ss.hasPermi('visitor:card:add')")
+    @PostMapping("/add")
+    public AjaxResult add(@RequestBody SysCardSaveRequest request)
+    {
+        return toAjax(cardService.save(request, getUsername()));
+    }
+
+    @Log(title = "卡片管理", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('visitor:card:edit')")
+    @PostMapping("/edit")
+    public AjaxResult edit(@RequestBody SysCardSaveRequest request)
+    {
+        return toAjax(cardService.save(request, getUsername()));
+    }
+
+    @Log(title = "卡片管理", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('visitor:card:bind')")
+    @PostMapping("/bind")
+    public AjaxResult bind(@RequestBody SysCardSaveRequest request)
+    {
+        return toAjax(cardService.save(request, getUsername()));
+    }
+
+    @Log(title = "卡片管理", businessType = BusinessType.UPDATE)
+    @PreAuthorize("@ss.hasPermi('visitor:card:unbind')")
+    @PostMapping("/unbind")
+    public AjaxResult unbind(@RequestBody SysCardSaveRequest request)
+    {
+        request.setEmployeeId(null);
+        request.setEmployeeName(null);
+        return toAjax(cardService.save(request, getUsername()));
+    }
+
+    @Log(title = "卡片管理", businessType = BusinessType.DELETE)
+    @PreAuthorize("@ss.hasPermi('visitor:card:remove')")
+    @PostMapping("/delete")
+    public AjaxResult delete(@RequestBody SysCardDeleteRequest request)
+    {
+        return toAjax(cardService.delete(request, getUsername()));
+    }
+
+    @Log(title = "卡片管理", businessType = BusinessType.IMPORT)
+    @PreAuthorize("@ss.hasPermi('visitor:card:import')")
+    @PostMapping("/importData")
+    public AjaxResult importData(@RequestParam("file") MultipartFile file) throws IOException
+    {
+        if (file == null || file.isEmpty())
+        {
+            return error("请选择导入文件");
+        }
+        return success(cardService.importData(file.getInputStream(), getUsername()));
+    }
+
+    @Log(title = "卡片管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('visitor:card:export')")
+    @PostMapping("/export")
+    public void export(@RequestBody(required = false) SysCardPageRequest request, HttpServletResponse response) throws IOException
+    {
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("卡片列表", "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+        List<SysCardVO> cards = cardService.list(request);
+        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        try (Workbook workbook = new XSSFWorkbook())
+        {
+            Sheet sheet = workbook.createSheet("卡片列表");
+            String[] headers = {"卡号", "员工姓名", "联系方式", "所属部门", "绑定时间", "卡片状态"};
+            Row headerRow = sheet.createRow(0);
+            for (int index = 0; index < headers.length; index++)
+            {
+                Cell cell = headerRow.createCell(index);
+                cell.setCellValue(headers[index]);
+                sheet.setColumnWidth(index, 22 * 256);
+            }
+            for (int index = 0; index < cards.size(); index++)
+            {
+                SysCardVO card = cards.get(index);
+                Row row = sheet.createRow(index + 1);
+                row.createCell(0).setCellValue(value(card.getCardNo()));
+                row.createCell(1).setCellValue(value(card.getEmployeeName()));
+                row.createCell(2).setCellValue(value(card.getContactPhone()));
+                row.createCell(3).setCellValue(value(card.getDeptName()));
+                row.createCell(4).setCellValue(card.getBindTime() == null ? "" : dateFormat.format(card.getBindTime()));
+                row.createCell(5).setCellValue(Boolean.TRUE.equals(card.getBound()) ? "已绑定" : "未绑定");
+            }
+            workbook.write(response.getOutputStream());
+        }
+    }
+
+    @Log(title = "卡片管理", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('visitor:card:import')")
+    @PostMapping("/importTemplate")
+    public void importTemplate(HttpServletResponse response) throws IOException
+    {
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String fileName = URLEncoder.encode("卡片导入模板", "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+        try (Workbook workbook = new XSSFWorkbook())
+        {
+            Sheet sheet = workbook.createSheet("卡片导入");
+            String[] headers = {"卡号", "员工姓名"};
+            Row headerRow = sheet.createRow(0);
+            for (int index = 0; index < headers.length; index++)
+            {
+                headerRow.createCell(index).setCellValue(headers[index]);
+                sheet.setColumnWidth(index, 24 * 256);
+            }
+            workbook.write(response.getOutputStream());
+        }
+    }
+
+    private String value(String value)
+    {
+        return value == null ? "" : value;
+    }
+}
diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml
index 1c00496..d6cd895 100644
--- a/ruoyi-admin/src/main/resources/application-prod.yml
+++ b/ruoyi-admin/src/main/resources/application-prod.yml
@@ -17,7 +17,7 @@
   # 部署模式:standalone单机版 cluster集群版
   deploymentMode: cluster
   # 集群版是否对接中台:true对接 false不对接
-  middlePlatformEnabled: false
+  middlePlatformEnabled: true
 # 开发环境配置
 server:
   # 服务器的HTTP端口,默认为8080
diff --git a/ruoyi-admin/src/main/resources/application-test.yml b/ruoyi-admin/src/main/resources/application-test.yml
index eccc799..1c90ae0 100644
--- a/ruoyi-admin/src/main/resources/application-test.yml
+++ b/ruoyi-admin/src/main/resources/application-test.yml
@@ -17,7 +17,7 @@
   # 部署模式:standalone单机版 cluster集群版
   deploymentMode: cluster
   # 集群版是否对接中台:true对接 false不对接
-  middlePlatformEnabled: false
+  middlePlatformEnabled: true
 # 开发环境配置
 server:
   # 服务器的HTTP端口,默认为8080
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index e673e36..dcc106d 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -1,4 +1,4 @@
 # 项目相关配置
 spring:
   profiles:
-    active: prod
+    active: test
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCard.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCard.java
new file mode 100644
index 0000000..e971a07
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCard.java
@@ -0,0 +1,23 @@
+package com.ruoyi.system.domain;
+
+import com.ruoyi.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 卡片管理实体,对应 t_card 表。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SysCard extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    private Long cardId;
+
+    private String cardNo;
+
+    private Long employeeId;
+
+    private String delFlag;
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDeleteRequest.java b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDeleteRequest.java
new file mode 100644
index 0000000..21ec92c
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDeleteRequest.java
@@ -0,0 +1,16 @@
+package com.ruoyi.system.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 卡片删除请求。
+ */
+@Data
+public class SysCardDeleteRequest
+{
+    @ApiModelProperty("卡片主键集合")
+    private List<Long> cardIds;
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDetailRequest.java b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDetailRequest.java
new file mode 100644
index 0000000..e500b24
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDetailRequest.java
@@ -0,0 +1,14 @@
+package com.ruoyi.system.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 卡片详情请求。
+ */
+@Data
+public class SysCardDetailRequest
+{
+    @ApiModelProperty("卡片主键")
+    private Long cardId;
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardEmployeeRequest.java b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardEmployeeRequest.java
new file mode 100644
index 0000000..4f75671
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardEmployeeRequest.java
@@ -0,0 +1,14 @@
+package com.ruoyi.system.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 按员工姓名查询绑定信息请求。
+ */
+@Data
+public class SysCardEmployeeRequest
+{
+    @ApiModelProperty("员工姓名")
+    private String employeeName;
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardPageRequest.java b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardPageRequest.java
new file mode 100644
index 0000000..18c1d22
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardPageRequest.java
@@ -0,0 +1,23 @@
+package com.ruoyi.system.dto;
+
+import com.ruoyi.common.core.domain.BasePage;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 卡片分页查询请求。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class SysCardPageRequest extends BasePage
+{
+    @ApiModelProperty("卡号")
+    private String cardNo;
+
+    @ApiModelProperty("员工姓名")
+    private String employeeName;
+
+    @ApiModelProperty("卡片是否已绑定")
+    private Boolean bound;
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardSaveRequest.java b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardSaveRequest.java
new file mode 100644
index 0000000..c02041b
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardSaveRequest.java
@@ -0,0 +1,23 @@
+package com.ruoyi.system.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 卡片新增或编辑请求。
+ */
+@Data
+public class SysCardSaveRequest
+{
+    @ApiModelProperty("卡片主键,编辑时必填")
+    private Long cardId;
+
+    @ApiModelProperty("卡号")
+    private String cardNo;
+
+    @ApiModelProperty("员工主键,绑定时必填")
+    private Long employeeId;
+
+    @ApiModelProperty("员工姓名,用于导入时校验")
+    private String employeeName;
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysCardMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysCardMapper.java
new file mode 100644
index 0000000..2904a4f
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysCardMapper.java
@@ -0,0 +1,41 @@
+package com.ruoyi.system.mapper;
+
+import com.ruoyi.common.basic.PageInfo;
+import com.ruoyi.system.domain.SysCard;
+import com.ruoyi.system.dto.SysCardPageRequest;
+import com.ruoyi.system.vo.SysCardVO;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 卡片管理数据层。
+ */
+@Mapper
+public interface SysCardMapper
+{
+    List<SysCardVO> pageList(@Param("query") SysCardPageRequest query, @Param("pageInfo") PageInfo<SysCardVO> pageInfo);
+
+    List<SysCardVO> selectList(@Param("query") SysCardPageRequest query);
+
+    SysCardVO selectVoById(Long cardId);
+
+    SysCard selectById(Long cardId);
+
+    SysCard selectByCardNo(@Param("cardNo") String cardNo);
+
+    SysCard selectByEmployeeId(@Param("employeeId") Long employeeId);
+
+    int insertCard(SysCard card);
+
+    int updateCard(SysCard card);
+
+    int deleteByIds(@Param("cardIds") List<Long> cardIds, @Param("updateBy") String updateBy);
+
+    int bindCard(@Param("cardId") Long cardId, @Param("employeeId") Long employeeId, @Param("updateBy") String updateBy);
+
+    int unbindByEmployeeId(@Param("employeeId") Long employeeId, @Param("updateBy") String updateBy);
+
+    int unbindByCardNo(@Param("cardNo") String cardNo, @Param("updateBy") String updateBy);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysEmployeeMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysEmployeeMapper.java
index 5e28f0b..4a9b2ce 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysEmployeeMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysEmployeeMapper.java
@@ -23,6 +23,8 @@
 
     public SysEmployeeVO selectVoById(Long employeeId);
 
+    public List<SysEmployeeVO> selectVoByName(@Param("employeeName") String employeeName);
+
     public SysEmployee selectById(Long employeeId);
 
     public SysEmployee checkEmployeeNoUnique(@Param("employeeNo") String employeeNo,
@@ -43,6 +45,9 @@
 
     public int clearCard(@Param("employeeIds") List<Long> employeeIds, @Param("updateBy") String updateBy);
 
+    public int updateCardNo(@Param("employeeId") Long employeeId, @Param("cardNo") String cardNo,
+                            @Param("updateBy") String updateBy);
+
     public int clearFace(@Param("employeeIds") List<Long> employeeIds, @Param("updateBy") String updateBy);
 
     public int deleteByIds(@Param("employeeIds") List<Long> employeeIds, @Param("updateBy") String updateBy);
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysCardService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysCardService.java
new file mode 100644
index 0000000..ea449cb
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysCardService.java
@@ -0,0 +1,32 @@
+package com.ruoyi.system.service;
+
+import com.ruoyi.common.basic.PageInfo;
+import com.ruoyi.system.dto.SysCardDeleteRequest;
+import com.ruoyi.system.dto.SysCardDetailRequest;
+import com.ruoyi.system.dto.SysCardEmployeeRequest;
+import com.ruoyi.system.dto.SysCardPageRequest;
+import com.ruoyi.system.dto.SysCardSaveRequest;
+import com.ruoyi.system.vo.SysCardVO;
+
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * 卡片管理服务。
+ */
+public interface ISysCardService
+{
+    PageInfo<SysCardVO> pageList(SysCardPageRequest request);
+
+    List<SysCardVO> list(SysCardPageRequest request);
+
+    SysCardVO detail(SysCardDetailRequest request);
+
+    SysCardVO findEmployee(SysCardEmployeeRequest request);
+
+    int save(SysCardSaveRequest request, String username);
+
+    int delete(SysCardDeleteRequest request, String username);
+
+    String importData(InputStream inputStream, String username);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysCardServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysCardServiceImpl.java
new file mode 100644
index 0000000..67de457
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysCardServiceImpl.java
@@ -0,0 +1,340 @@
+package com.ruoyi.system.service.impl;
+
+import com.ruoyi.common.basic.PageInfo;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.domain.SysCard;
+import com.ruoyi.system.domain.SysEmployee;
+import com.ruoyi.system.dto.SysCardDeleteRequest;
+import com.ruoyi.system.dto.SysCardDetailRequest;
+import com.ruoyi.system.dto.SysCardEmployeeRequest;
+import com.ruoyi.system.dto.SysCardPageRequest;
+import com.ruoyi.system.dto.SysCardSaveRequest;
+import com.ruoyi.system.mapper.SysCardMapper;
+import com.ruoyi.system.mapper.SysEmployeeMapper;
+import com.ruoyi.system.service.ISysCardService;
+import com.ruoyi.system.utils.EmployeePhoneCrypto;
+import com.ruoyi.system.vo.SysCardVO;
+import com.ruoyi.system.vo.SysEmployeeVO;
+import org.apache.poi.ss.usermodel.DataFormatter;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.usermodel.WorkbookFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 卡片管理服务实现。
+ */
+@Service
+public class SysCardServiceImpl implements ISysCardService
+{
+    @Autowired
+    private SysCardMapper cardMapper;
+
+    @Autowired
+    private SysEmployeeMapper employeeMapper;
+
+    @Override
+    public PageInfo<SysCardVO> pageList(SysCardPageRequest request)
+    {
+        SysCardPageRequest query = normalizeQuery(request);
+        PageInfo<SysCardVO> pageInfo = new PageInfo<SysCardVO>(query.getPageNum(), query.getPageSize());
+        List<SysCardVO> rows = cardMapper.pageList(query, pageInfo);
+        fillDisplayFields(rows, false);
+        pageInfo.setRecords(rows);
+        return pageInfo;
+    }
+
+    @Override
+    public List<SysCardVO> list(SysCardPageRequest request)
+    {
+        List<SysCardVO> rows = cardMapper.selectList(normalizeQuery(request));
+        fillDisplayFields(rows, true);
+        return rows;
+    }
+
+    @Override
+    public SysCardVO detail(SysCardDetailRequest request)
+    {
+        Long cardId = requiredId(request == null ? null : request.getCardId(), "卡片主键不能为空");
+        SysCardVO card = cardMapper.selectVoById(cardId);
+        if (card == null)
+        {
+            throw new ServiceException("卡片不存在");
+        }
+        fillDisplayFields(Collections.singletonList(card), true);
+        return card;
+    }
+
+    @Override
+    public SysCardVO findEmployee(SysCardEmployeeRequest request)
+    {
+        String employeeName = requiredText(request == null ? null : request.getEmployeeName(), "请输入员工姓名");
+        List<SysEmployeeVO> employees = employeeMapper.selectVoByName(employeeName);
+        if (employees == null || employees.isEmpty())
+        {
+            throw new ServiceException("该员工不存在,请到员工管理中添加");
+        }
+        if (employees.size() > 1)
+        {
+            throw new ServiceException("员工姓名重复,请使用员工管理中的唯一姓名");
+        }
+        SysEmployeeVO employee = employees.get(0);
+        employee.setContactPhone(EmployeePhoneCrypto.decrypt(employee.getContactPhone()));
+        SysCardVO result = new SysCardVO();
+        result.setEmployeeId(employee.getEmployeeId());
+        result.setEmployeeName(employee.getEmployeeName());
+        result.setContactPhone(employee.getContactPhone());
+        result.setDeptId(employee.getDeptId());
+        result.setDeptName(employee.getDeptName());
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int save(SysCardSaveRequest request, String username)
+    {
+        if (request == null)
+        {
+            throw new ServiceException("卡片内容不能为空");
+        }
+        String cardNo = requiredText(request.getCardNo(), "卡号不能为空");
+        SysCard existing = request.getCardId() == null ? null : cardMapper.selectById(request.getCardId());
+        if (request.getCardId() != null && existing == null)
+        {
+            throw new ServiceException("卡片不存在");
+        }
+
+        Long employeeId = resolveEmployeeId(request);
+        SysCard duplicated = cardMapper.selectByCardNo(cardNo);
+        if (duplicated != null && (existing == null || !duplicated.getCardId().equals(existing.getCardId())))
+        {
+            throw new ServiceException("卡号已存在");
+        }
+
+        SysEmployee employee = employeeId == null ? null : employeeMapper.selectById(employeeId);
+        if (employeeId != null && employee == null)
+        {
+            throw new ServiceException("该员工不存在,请到员工管理中添加");
+        }
+        if (employeeId != null)
+        {
+            if (StringUtils.isNotEmpty(StringUtils.trim(request.getEmployeeName()))
+                    && !StringUtils.trim(request.getEmployeeName()).equals(employee.getEmployeeName()))
+            {
+                throw new ServiceException("员工信息已变更,请重新校验员工姓名");
+            }
+            boolean editingEmployeeCard = existing != null && employeeId.equals(existing.getEmployeeId());
+            if (StringUtils.isNotEmpty(employee.getCardNo())
+                    && !cardNo.equals(employee.getCardNo())
+                    && !editingEmployeeCard)
+            {
+                throw new ServiceException("该员工已绑定其他卡片");
+            }
+            SysCard employeeCard = cardMapper.selectByEmployeeId(employeeId);
+            if (employeeCard != null && (existing == null || !employeeCard.getCardId().equals(existing.getCardId())))
+            {
+                throw new ServiceException("该员工已绑定其他卡片");
+            }
+            SysEmployee sameCardEmployee = employeeMapper.checkCardNoUnique(cardNo, employeeId);
+            if (sameCardEmployee != null)
+            {
+                throw new ServiceException("卡号已绑定其他员工");
+            }
+        }
+
+        if (existing == null)
+        {
+            SysCard card = new SysCard();
+            card.setCardNo(cardNo);
+            card.setEmployeeId(employeeId);
+            card.setCreateBy(username);
+            int rows = cardMapper.insertCard(card);
+            if (employeeId != null)
+            {
+                employeeMapper.updateCardNo(employeeId, cardNo, username);
+            }
+            return rows;
+        }
+
+        String oldCardNo = existing.getCardNo();
+        Long oldEmployeeId = existing.getEmployeeId();
+        if (oldEmployeeId != null && (!oldEmployeeId.equals(employeeId) || !oldCardNo.equals(cardNo)))
+        {
+            clearEmployeeCard(oldEmployeeId, oldCardNo, username);
+            cardMapper.unbindByEmployeeId(oldEmployeeId, username);
+        }
+        existing.setCardNo(cardNo);
+        existing.setUpdateBy(username);
+        int rows = cardMapper.updateCard(existing);
+        if (employeeId == null)
+        {
+            cardMapper.unbindByCardNo(cardNo, username);
+        }
+        else
+        {
+            cardMapper.bindCard(existing.getCardId(), employeeId, username);
+            employeeMapper.updateCardNo(employeeId, cardNo, username);
+        }
+        return rows;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int delete(SysCardDeleteRequest request, String username)
+    {
+        List<Long> cardIds = request == null ? null : request.getCardIds();
+        if (cardIds == null || cardIds.isEmpty())
+        {
+            throw new ServiceException("请选择卡片");
+        }
+        for (Long cardId : cardIds)
+        {
+            SysCard card = cardMapper.selectById(requiredId(cardId, "卡片主键不能为空"));
+            if (card != null && card.getEmployeeId() != null)
+            {
+                clearEmployeeCard(card.getEmployeeId(), card.getCardNo(), username);
+            }
+        }
+        return cardMapper.deleteByIds(cardIds, username);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String importData(InputStream inputStream, String username)
+    {
+        int successCount = 0;
+        List<String> errors = new ArrayList<String>();
+        DataFormatter formatter = new DataFormatter();
+        try (Workbook workbook = WorkbookFactory.create(inputStream))
+        {
+            Sheet sheet = workbook.getSheetAt(0);
+            if (sheet == null || sheet.getLastRowNum() < 1)
+            {
+                throw new ServiceException("导入文件没有卡片数据");
+            }
+            for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++)
+            {
+                Row row = sheet.getRow(rowIndex);
+                String cardNo = getCell(row, 0, formatter);
+                String employeeName = getCell(row, 1, formatter);
+                if (StringUtils.isEmpty(cardNo) && StringUtils.isEmpty(employeeName))
+                {
+                    continue;
+                }
+                try
+                {
+                    SysCardSaveRequest request = new SysCardSaveRequest();
+                    request.setCardNo(cardNo);
+                    request.setEmployeeName(employeeName);
+                    save(request, username);
+                    successCount++;
+                }
+                catch (Exception e)
+                {
+                    errors.add("第" + (rowIndex + 1) + "行:" + e.getMessage());
+                }
+            }
+        }
+        catch (ServiceException e)
+        {
+            throw e;
+        }
+        catch (Exception e)
+        {
+            throw new ServiceException("卡片导入失败:" + e.getMessage());
+        }
+        if (successCount == 0 && errors.isEmpty())
+        {
+            throw new ServiceException("导入文件没有有效卡片数据");
+        }
+        if (errors.isEmpty())
+        {
+            return "导入成功,共导入" + successCount + "条卡片数据";
+        }
+        return "导入完成,成功" + successCount + "条,失败" + errors.size() + "条;" + String.join(";", errors);
+    }
+
+    private Long resolveEmployeeId(SysCardSaveRequest request)
+    {
+        if (request.getEmployeeId() != null)
+        {
+            return request.getEmployeeId();
+        }
+        if (StringUtils.isEmpty(StringUtils.trim(request.getEmployeeName())))
+        {
+            return null;
+        }
+        SysCardEmployeeRequest employeeRequest = new SysCardEmployeeRequest();
+        employeeRequest.setEmployeeName(request.getEmployeeName());
+        return findEmployee(employeeRequest).getEmployeeId();
+    }
+
+    private void clearEmployeeCard(Long employeeId, String cardNo, String username)
+    {
+        SysEmployee employee = employeeMapper.selectById(employeeId);
+        if (employee != null && cardNo != null && cardNo.equals(employee.getCardNo()))
+        {
+            employeeMapper.updateCardNo(employeeId, null, username);
+        }
+    }
+
+    private SysCardPageRequest normalizeQuery(SysCardPageRequest request)
+    {
+        SysCardPageRequest query = request == null ? new SysCardPageRequest() : request;
+        query.setCardNo(StringUtils.trim(query.getCardNo()));
+        query.setEmployeeName(StringUtils.trim(query.getEmployeeName()));
+        return query;
+    }
+
+    private void fillDisplayFields(List<SysCardVO> cards, boolean fullPhone)
+    {
+        for (SysCardVO card : cards)
+        {
+            String phone = EmployeePhoneCrypto.decrypt(card.getContactPhone());
+            card.setContactPhone(fullPhone ? phone : maskPhone(phone));
+            card.setBound(card.getEmployeeId() != null);
+        }
+    }
+
+    private String maskPhone(String phone)
+    {
+        if (phone == null || phone.length() < 7)
+        {
+            return "";
+        }
+        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
+    }
+
+    private String requiredText(String value, String message)
+    {
+        String result = StringUtils.trim(value);
+        if (StringUtils.isEmpty(result))
+        {
+            throw new ServiceException(message);
+        }
+        return result;
+    }
+
+    private Long requiredId(Long value, String message)
+    {
+        if (value == null || value <= 0)
+        {
+            throw new ServiceException(message);
+        }
+        return value;
+    }
+
+    private String getCell(Row row, int index, DataFormatter formatter)
+    {
+        return row == null ? "" : StringUtils.trim(formatter.formatCellValue(row.getCell(index)));
+    }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysEmployeeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysEmployeeServiceImpl.java
index 6e39eea..cf2f15d 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysEmployeeServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysEmployeeServiceImpl.java
@@ -5,6 +5,7 @@
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.system.domain.SysEmployee;
+import com.ruoyi.system.domain.SysCard;
 import com.ruoyi.system.dto.SysEmployeeAddRequest;
 import com.ruoyi.system.dto.SysEmployeeDeleteRequest;
 import com.ruoyi.system.dto.SysEmployeeDetailRequest;
@@ -14,6 +15,7 @@
 import com.ruoyi.system.dto.SysEmployeeUnbindRequest;
 import com.ruoyi.system.mapper.SysDeptMapper;
 import com.ruoyi.system.mapper.SysEmployeeMapper;
+import com.ruoyi.system.mapper.SysCardMapper;
 import com.ruoyi.system.service.ISysEmployeeService;
 import com.ruoyi.system.utils.EmployeePhoneCrypto;
 import com.ruoyi.system.vo.SysEmployeeVO;
@@ -24,6 +26,7 @@
 import org.apache.poi.ss.usermodel.WorkbookFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
 import java.io.InputStream;
@@ -45,6 +48,9 @@
 
     @Autowired
     private SysEmployeeMapper employeeMapper;
+
+    @Autowired
+    private SysCardMapper cardMapper;
 
     @Autowired
     private SysDeptMapper deptMapper;
@@ -83,6 +89,7 @@
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int add(SysEmployeeAddRequest request, String username)
     {
         if (request == null)
@@ -93,10 +100,13 @@
                 request.getContactPhone(), request.getPosition(), request.getCardNo(), request.getFaceImage(),
                 request.getEnabled(), null);
         employee.setCreateBy(username);
-        return employeeMapper.insertEmployee(employee);
+        int rows = employeeMapper.insertEmployee(employee);
+        syncCard(employee.getEmployeeId(), null, employee.getCardNo(), username);
+        return rows;
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int edit(SysEmployeeEditRequest request, String username)
     {
         if (request == null)
@@ -108,12 +118,15 @@
         {
             throw new ServiceException("员工不存在");
         }
+        SysEmployee oldEmployee = employeeMapper.selectById(employeeId);
         SysEmployee employee = buildEmployee(request.getDeptId(), request.getEmployeeName(), request.getEmployeeNo(),
                 request.getContactPhone(), request.getPosition(), request.getCardNo(), request.getFaceImage(),
                 request.getEnabled(), employeeId);
         employee.setEmployeeId(employeeId);
         employee.setUpdateBy(username);
-        return employeeMapper.updateEmployee(employee);
+        int rows = employeeMapper.updateEmployee(employee);
+        syncCard(employeeId, oldEmployee.getCardNo(), employee.getCardNo(), username);
+        return rows;
     }
 
     @Override
@@ -128,9 +141,16 @@
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int unbindCard(SysEmployeeUnbindRequest request, String username)
     {
-        return employeeMapper.clearCard(getRequiredEmployeeIds(request == null ? null : request.getEmployeeIds()), username);
+        List<Long> employeeIds = getRequiredEmployeeIds(request == null ? null : request.getEmployeeIds());
+        int rows = employeeMapper.clearCard(employeeIds, username);
+        for (Long employeeId : employeeIds)
+        {
+            cardMapper.unbindByEmployeeId(employeeId, username);
+        }
+        return rows;
     }
 
     @Override
@@ -140,9 +160,16 @@
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public int delete(SysEmployeeDeleteRequest request, String username)
     {
-        return employeeMapper.deleteByIds(getRequiredEmployeeIds(request == null ? null : request.getEmployeeIds()), username);
+        List<Long> employeeIds = getRequiredEmployeeIds(request == null ? null : request.getEmployeeIds());
+        int rows = employeeMapper.deleteByIds(employeeIds, username);
+        for (Long employeeId : employeeIds)
+        {
+            cardMapper.unbindByEmployeeId(employeeId, username);
+        }
+        return rows;
     }
 
     @Override
@@ -369,6 +396,36 @@
         return StringUtils.trim(value);
     }
 
+    /**
+     * 员工模块直接维护卡号时同步卡片关系,保证两个管理入口的数据一致。
+     */
+    private void syncCard(Long employeeId, String oldCardNo, String newCardNo, String username)
+    {
+        if (oldCardNo != null && !oldCardNo.equals(newCardNo))
+        {
+            cardMapper.unbindByEmployeeId(employeeId, username);
+        }
+        if (newCardNo == null || newCardNo.length() == 0)
+        {
+            return;
+        }
+        SysCard card = cardMapper.selectByCardNo(newCardNo);
+        if (card == null)
+        {
+            card = new SysCard();
+            card.setCardNo(newCardNo);
+            card.setEmployeeId(employeeId);
+            card.setCreateBy(username);
+            cardMapper.insertCard(card);
+            return;
+        }
+        if (card.getEmployeeId() != null && !employeeId.equals(card.getEmployeeId()))
+        {
+            throw new ServiceException("卡号已绑定其他员工");
+        }
+        cardMapper.bindCard(card.getCardId(), employeeId, username);
+    }
+
     private void fillDisplayFields(SysEmployeeVO employee, boolean fullPhone)
     {
         String phone = EmployeePhoneCrypto.decrypt(employee.getContactPhone());
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/vo/SysCardVO.java b/ruoyi-system/src/main/java/com/ruoyi/system/vo/SysCardVO.java
new file mode 100644
index 0000000..c9dda25
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/vo/SysCardVO.java
@@ -0,0 +1,32 @@
+package com.ruoyi.system.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 卡片管理返回对象。
+ */
+@Data
+public class SysCardVO
+{
+    private Long cardId;
+
+    private String cardNo;
+
+    private Long employeeId;
+
+    private String employeeName;
+
+    private String contactPhone;
+
+    private Long deptId;
+
+    private String deptName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date bindTime;
+
+    private Boolean bound;
+}
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysCardMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysCardMapper.xml
new file mode 100644
index 0000000..92db048
--- /dev/null
+++ b/ruoyi-system/src/main/resources/mapper/system/SysCardMapper.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ruoyi.system.mapper.SysCardMapper">
+
+    <resultMap type="SysCard" id="SysCardResult">
+        <id property="cardId" column="card_id" />
+        <result property="cardNo" column="card_no" />
+        <result property="employeeId" column="employee_id" />
+        <result property="delFlag" column="del_flag" />
+        <result property="createBy" column="create_by" />
+        <result property="createTime" column="create_time" />
+        <result property="updateBy" column="update_by" />
+        <result property="updateTime" column="update_time" />
+    </resultMap>
+
+    <resultMap type="SysCardVO" id="SysCardVOResult">
+        <id property="cardId" column="card_id" />
+        <result property="cardNo" column="card_no" />
+        <result property="employeeId" column="employee_id" />
+        <result property="employeeName" column="employee_name" />
+        <result property="contactPhone" column="contact_phone_cipher" />
+        <result property="deptId" column="dept_id" />
+        <result property="deptName" column="dept_name" />
+        <result property="bindTime" column="bind_time" />
+        <result property="bound" column="bound" />
+    </resultMap>
+
+    <sql id="selectCardVo">
+        select c.card_id,
+               c.card_no,
+               c.employee_id,
+               e.employee_name,
+               e.contact_phone_cipher,
+               e.dept_id,
+               d.dept_name,
+               c.bind_time,
+               case when c.employee_id is not null then 1 else 0 end as bound
+        from t_card c
+        left join t_employee e on e.employee_id = c.employee_id and e.del_flag = '0'
+        left join sys_dept d on d.dept_id = e.dept_id and d.del_flag = '0'
+    </sql>
+
+    <select id="pageList" resultMap="SysCardVOResult">
+        <include refid="selectCardVo" />
+        <where>
+            c.del_flag = '0'
+            <if test="query.cardNo != null and query.cardNo != ''">
+                and c.card_no like concat('%', #{query.cardNo}, '%')
+            </if>
+            <if test="query.employeeName != null and query.employeeName != ''">
+                and e.employee_name like concat('%', #{query.employeeName}, '%')
+            </if>
+            <if test="query.bound != null and query.bound">
+                and c.employee_id is not null
+            </if>
+            <if test="query.bound != null and !query.bound">
+                and c.employee_id is null
+            </if>
+        </where>
+        order by c.bind_time desc, c.card_id desc
+    </select>
+
+    <select id="selectList" resultMap="SysCardVOResult">
+        <include refid="selectCardVo" />
+        <where>
+            c.del_flag = '0'
+            <if test="query.cardNo != null and query.cardNo != ''">
+                and c.card_no like concat('%', #{query.cardNo}, '%')
+            </if>
+            <if test="query.employeeName != null and query.employeeName != ''">
+                and e.employee_name like concat('%', #{query.employeeName}, '%')
+            </if>
+            <if test="query.bound != null and query.bound">
+                and c.employee_id is not null
+            </if>
+            <if test="query.bound != null and !query.bound">
+                and c.employee_id is null
+            </if>
+        </where>
+        order by c.bind_time desc, c.card_id desc
+    </select>
+
+    <select id="selectVoById" parameterType="Long" resultMap="SysCardVOResult">
+        <include refid="selectCardVo" />
+        where c.card_id = #{cardId}
+          and c.del_flag = '0'
+    </select>
+
+    <select id="selectById" parameterType="Long" resultMap="SysCardResult">
+        select card_id, card_no, employee_id, del_flag, create_by, create_time, update_by, update_time
+        from t_card
+        where card_id = #{cardId}
+          and del_flag = '0'
+    </select>
+
+    <select id="selectByCardNo" resultMap="SysCardResult">
+        select card_id, card_no, employee_id, del_flag, create_by, create_time, update_by, update_time
+        from t_card
+        where card_no = #{cardNo}
+          and del_flag = '0'
+        limit 1
+    </select>
+
+    <select id="selectByEmployeeId" resultMap="SysCardResult">
+        select card_id, card_no, employee_id, del_flag, create_by, create_time, update_by, update_time
+        from t_card
+        where employee_id = #{employeeId}
+          and del_flag = '0'
+        limit 1
+    </select>
+
+    <insert id="insertCard" parameterType="SysCard" useGeneratedKeys="true" keyProperty="cardId">
+        insert into t_card (card_no, employee_id, bind_time, create_by, create_time)
+        values (#{cardNo}, #{employeeId},
+                <choose>
+                    <when test="employeeId != null">sysdate()</when>
+                    <otherwise>null</otherwise>
+                </choose>,
+                #{createBy}, sysdate())
+    </insert>
+
+    <update id="updateCard" parameterType="SysCard">
+        update t_card
+        set card_no = #{cardNo},
+            update_by = #{updateBy},
+            update_time = sysdate()
+        where card_id = #{cardId}
+          and del_flag = '0'
+    </update>
+
+    <update id="bindCard">
+        update t_card
+        set employee_id = #{employeeId},
+            bind_time = sysdate(),
+            update_by = #{updateBy},
+            update_time = sysdate()
+        where card_id = #{cardId}
+          and del_flag = '0'
+    </update>
+
+    <update id="unbindByEmployeeId">
+        update t_card
+        set employee_id = null,
+            bind_time = null,
+            update_by = #{updateBy},
+            update_time = sysdate()
+        where employee_id = #{employeeId}
+          and del_flag = '0'
+    </update>
+
+    <update id="unbindByCardNo">
+        update t_card
+        set employee_id = null,
+            bind_time = null,
+            update_by = #{updateBy},
+            update_time = sysdate()
+        where card_no = #{cardNo}
+          and del_flag = '0'
+    </update>
+
+    <update id="deleteByIds">
+        update t_card
+        set employee_id = null,
+            bind_time = null,
+            del_flag = '1',
+            update_by = #{updateBy},
+            update_time = sysdate()
+        where del_flag = '0'
+          and card_id in
+        <foreach collection="cardIds" item="cardId" open="(" separator="," close=")">
+            #{cardId}
+        </foreach>
+    </update>
+</mapper>
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysEmployeeMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysEmployeeMapper.xml
index 5c5b802..9c31955 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysEmployeeMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysEmployeeMapper.xml
@@ -108,6 +108,13 @@
           and e.del_flag = '0'
     </select>
 
+    <select id="selectVoByName" resultMap="SysEmployeeVOResult">
+        <include refid="selectEmployeeVo" />
+        where e.employee_name = #{employeeName}
+          and e.del_flag = '0'
+        order by e.employee_id
+    </select>
+
     <select id="selectById" parameterType="Long" resultMap="SysEmployeeResult">
         <include refid="selectEmployeeDomain" />
         where employee_id = #{employeeId}
@@ -213,6 +220,15 @@
         </foreach>
     </update>
 
+    <update id="updateCardNo">
+        update t_employee
+        set card_no = #{cardNo},
+            update_by = #{updateBy},
+            update_time = sysdate()
+        where employee_id = #{employeeId}
+          and del_flag = '0'
+    </update>
+
     <update id="clearFace">
         update t_employee
         set face_image = null,
diff --git a/ruoyi-system/src/main/resources/mapper/system/SysVisitorConfigMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysVisitorConfigMapper.xml
index 08aedde..d288ed7 100644
--- a/ruoyi-system/src/main/resources/mapper/system/SysVisitorConfigMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/SysVisitorConfigMapper.xml
@@ -122,7 +122,8 @@
             visitor_machine_entry_enabled = #{visitorMachineEntryEnabled},
             update_by = #{updateBy},
             update_time = sysdate(),
-            remark = #{remark}
+            remark = #{remark},
+            center_path = #{centerPath}
         where config_id = #{configId}
     </update>
 </mapper>
diff --git a/ruoyi-ui/.env.production b/ruoyi-ui/.env.production
index d819a67..e0b403b 100644
--- a/ruoyi-ui/.env.production
+++ b/ruoyi-ui/.env.production
@@ -11,6 +11,6 @@
 VUE_APP_PUBLIC_PATH = './'
 
 VUE_APP_VISITOR_FRONTEND_MODE = 'cluster'
-VUE_APP_VISITOR_MIDDLE_PLATFORM_ENABLED = 'false'
+VUE_APP_VISITOR_MIDDLE_PLATFORM_ENABLED = 'true'
 # VUE_APP_VISITOR_LOGIN_MIDDLE_PLATFORM_ENABLED 未配置时默认跟随 VUE_APP_VISITOR_MIDDLE_PLATFORM_ENABLED
 VUE_APP_VISITOR_API_BASE_URL = '/prod-api'
diff --git a/ruoyi-ui/dist.zip b/ruoyi-ui/dist.zip
new file mode 100644
index 0000000..40eccb5
--- /dev/null
+++ b/ruoyi-ui/dist.zip
Binary files differ
diff --git a/ruoyi-ui/src/api/visitor-admin/card.js b/ruoyi-ui/src/api/visitor-admin/card.js
new file mode 100644
index 0000000..32bbbbd
--- /dev/null
+++ b/ruoyi-ui/src/api/visitor-admin/card.js
@@ -0,0 +1,69 @@
+import request from '@/utils/request'
+
+export function listCard(data) {
+  return request({ url: '/system/card/page', method: 'post', data })
+}
+
+export function getCard(cardId) {
+  return request({
+    url: '/system/card/detail',
+    method: 'post',
+    data: {
+      cardId: cardId
+    }
+  })
+}
+
+export function findCardEmployee(employeeName) {
+  return request({
+    url: '/system/card/employee',
+    method: 'post',
+    data: {
+      employeeName: employeeName
+    },
+    silentError: true
+  })
+}
+
+export function addCard(data) {
+  return request({ url: '/system/card/add', method: 'post', data })
+}
+
+export function updateCard(data) {
+  return request({ url: '/system/card/edit', method: 'post', data })
+}
+
+export function bindCard(data) {
+  return request({ url: '/system/card/bind', method: 'post', data })
+}
+
+export function unbindCard(data) {
+  return request({ url: '/system/card/unbind', method: 'post', data })
+}
+
+export function delCard(cardIds) {
+  return request({
+    url: '/system/card/delete',
+    method: 'post',
+    data: {
+      cardIds: cardIds
+    }
+  })
+}
+
+export function importCardData(data) {
+  return request({
+    url: '/system/card/importData',
+    method: 'post',
+    data,
+    headers: { 'Content-Type': 'multipart/form-data' }
+  })
+}
+
+export function exportCard(data) {
+  return request({ url: '/system/card/export', method: 'post', data, responseType: 'blob' })
+}
+
+export function downloadCardTemplate() {
+  return request({ url: '/system/card/importTemplate', method: 'post', responseType: 'blob' })
+}
diff --git a/ruoyi-ui/src/router/index.js b/ruoyi-ui/src/router/index.js
index a1da171..3cb81ec 100644
--- a/ruoyi-ui/src/router/index.js
+++ b/ruoyi-ui/src/router/index.js
@@ -148,6 +148,12 @@
         component: () => import('@/views/visitor-admin/device/index'),
         name: 'VisitorDoorDevice',
         meta: { title: '门禁管理', icon: 'server', deviceType: 'door' }
+      },
+      {
+        path: 'card',
+        component: () => import('@/views/visitor-admin/card/index'),
+        name: 'VisitorCard',
+        meta: { title: '卡片管理', icon: 'bank-card' }
       }
     ]
   },
diff --git a/ruoyi-ui/src/views/visitor-admin/card/index.vue b/ruoyi-ui/src/views/visitor-admin/card/index.vue
new file mode 100644
index 0000000..2a53421
--- /dev/null
+++ b/ruoyi-ui/src/views/visitor-admin/card/index.vue
@@ -0,0 +1,249 @@
+<template>
+  <main class="visitor-page card-page">
+    <section class="visitor-card">
+      <header class="visitor-header">
+        <h1>卡片管理</h1>
+      </header>
+
+      <el-form ref="queryForm" :model="queryParams" size="small" :inline="true" class="visitor-query">
+        <el-form-item label="卡号" prop="cardNo">
+          <el-input v-model="queryParams.cardNo" placeholder="请输入" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="姓名" prop="employeeName">
+          <el-input v-model="queryParams.employeeName" placeholder="请输入" clearable @keyup.enter.native="handleQuery" />
+        </el-form-item>
+        <el-form-item label="卡片状态" prop="bound">
+          <el-select v-model="queryParams.bound" placeholder="全部" clearable>
+            <el-option label="已绑定" :value="true" />
+            <el-option label="未绑定" :value="false" />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="resetQuery">重置</el-button>
+          <el-button type="primary" @click="handleQuery">搜索</el-button>
+        </el-form-item>
+      </el-form>
+
+      <div class="card-toolbar">
+        <div>
+          <el-button v-hasPermi="['visitor:card:import']" type="primary" plain icon="el-icon-upload2" size="small" @click="handleImport">批量导入</el-button>
+          <el-button v-hasPermi="['visitor:card:import']" type="text" icon="el-icon-download" size="small" @click="handleDownloadTemplate">下载模板</el-button>
+          <el-button v-hasPermi="['visitor:card:export']" type="primary" plain icon="el-icon-document" size="small" @click="handleExport">导出</el-button>
+          <input ref="cardImportFile" class="card-import-file" type="file" accept=".xlsx,.xls" @change="handleImportFileChange">
+        </div>
+        <el-button v-hasPermi="['visitor:card:add']" type="primary" icon="el-icon-plus" size="small" @click="handleAdd">绑定卡片</el-button>
+      </div>
+
+      <el-table v-loading="loading" class="visitor-table" :data="cardList">
+        <el-table-column label="序号" width="64" align="center">
+          <template slot-scope="scope">{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</template>
+        </el-table-column>
+        <el-table-column label="卡号" prop="cardNo" min-width="150" show-overflow-tooltip />
+        <el-table-column label="员工姓名" prop="employeeName" min-width="110" show-overflow-tooltip />
+        <el-table-column label="联系方式" prop="contactPhone" min-width="130" />
+        <el-table-column label="所属部门" prop="deptName" min-width="140" show-overflow-tooltip />
+        <el-table-column label="绑定时间" prop="bindTime" min-width="165" />
+        <el-table-column label="卡片状态" align="center" width="105">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.bound ? 'success' : 'warning'">{{ scope.row.bound ? '已绑定' : '未绑定' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column v-if="$auth.hasPermi('visitor:card:edit') || $auth.hasPermi('visitor:card:bind') || $auth.hasPermi('visitor:card:unbind') || $auth.hasPermi('visitor:card:remove')" label="操作" align="center" width="180" fixed="right">
+          <template slot-scope="scope">
+            <el-button v-hasPermi="['visitor:card:edit']" type="text" @click="handleUpdate(scope.row)">编辑</el-button>
+            <el-button v-if="scope.row.bound" v-hasPermi="['visitor:card:unbind']" type="text" @click="handleUnbind(scope.row)">解绑</el-button>
+            <el-button v-else v-hasPermi="['visitor:card:bind']" type="text" @click="handleUpdate(scope.row)">绑定</el-button>
+            <el-button v-hasPermi="['visitor:card:remove']" type="text" class="danger-text" @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+    </section>
+
+    <el-dialog :title="title" :visible.sync="open" width="560px" append-to-body custom-class="visitor-card-dialog">
+      <el-form ref="form" :model="form" :rules="rules" label-position="top">
+        <el-form-item label="卡号" prop="cardNo">
+          <el-input v-model="form.cardNo" placeholder="请输入卡号" />
+        </el-form-item>
+        <el-form-item label="员工姓名" prop="employeeName">
+          <el-input v-model="form.employeeName" placeholder="请输入员工姓名" @blur="handleEmployeeBlur" />
+          <div v-if="employeeError" class="card-form-error">{{ employeeError }}</div>
+        </el-form-item>
+        <el-form-item label="所属部门">
+          <el-input v-model="form.deptName" disabled placeholder="员工校验后自动带出" />
+        </el-form-item>
+        <el-form-item label="联系方式">
+          <el-input v-model="form.contactPhone" disabled placeholder="员工校验后自动带出" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="open = false">取消</el-button>
+        <el-button type="primary" @click="submitForm">保存</el-button>
+      </div>
+    </el-dialog>
+  </main>
+</template>
+
+<script>
+import { saveAs } from 'file-saver'
+import { listCard, getCard, findCardEmployee, addCard, updateCard, bindCard, unbindCard, delCard, importCardData, exportCard, downloadCardTemplate } from '@/api/visitor-admin/card'
+import { getPageRows, getPageTotal, getResponseData } from '@/utils/visitor-admin'
+
+export default {
+  name: 'VisitorCard',
+  data() {
+    return {
+      loading: false,
+      open: false,
+      title: '',
+      employeeError: '',
+      total: 0,
+      cardList: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        cardNo: '',
+        employeeName: '',
+        bound: undefined
+      },
+      form: {},
+      rules: {
+        cardNo: [{ required: true, message: '请输入卡号', trigger: 'blur' }],
+        employeeName: [{ required: true, message: '请输入员工姓名', trigger: 'blur' }]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    getList() {
+      this.loading = true
+      listCard(this.queryParams).then(res => {
+        this.cardList = getPageRows(res)
+        this.total = getPageTotal(res)
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.queryParams.bound = undefined
+      this.handleQuery()
+    },
+    handleAdd() {
+      this.title = '绑定卡片'
+      this.form = { cardNo: '', employeeName: '', employeeId: null, deptName: '', contactPhone: '' }
+      this.employeeError = ''
+      this.open = true
+    },
+    handleUpdate(row) {
+      getCard(row.cardId).then(res => {
+        this.title = row.bound ? '编辑卡片' : '绑定卡片'
+        this.form = getResponseData(res) || {}
+        this.employeeError = ''
+        this.open = true
+      })
+    },
+    handleEmployeeBlur() {
+      const employeeName = (this.form.employeeName || '').trim()
+      this.employeeError = ''
+      this.form.employeeId = null
+      this.form.deptName = ''
+      this.form.contactPhone = ''
+      if (!employeeName) {
+        return
+      }
+      findCardEmployee(employeeName).then(res => {
+        const employee = getResponseData(res) || {}
+        this.form.employeeId = employee.employeeId
+        this.form.employeeName = employee.employeeName
+        this.form.deptName = employee.deptName
+        this.form.contactPhone = employee.contactPhone
+      }).catch(error => {
+        this.employeeError = (error && error.message) || '该员工不存在,请到员工管理中添加'
+      })
+    },
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (!valid) {
+          return
+        }
+        if (!this.form.employeeId) {
+          this.employeeError = '该员工不存在,请先到员工管理中添加'
+          return
+        }
+        const request = this.form.cardId ? (this.form.bound ? updateCard : bindCard) : addCard
+        request({ cardId: this.form.cardId, cardNo: this.form.cardNo, employeeId: this.form.employeeId, employeeName: this.form.employeeName }).then(() => {
+          this.$modal.msgSuccess('保存成功')
+          this.open = false
+          this.getList()
+        })
+      })
+    },
+    handleUnbind(row) {
+      this.$modal.confirm('解绑后卡片将变为未绑定状态,确认解绑吗?').then(() => unbindCard({ cardId: row.cardId, cardNo: row.cardNo })).then(() => {
+        this.$modal.msgSuccess('解绑成功')
+        this.getList()
+      })
+    },
+    handleDelete(row) {
+      this.$modal.confirm('是否确认删除该卡片?').then(() => delCard([row.cardId])).then(() => {
+        this.$modal.msgSuccess('删除成功')
+        this.getList()
+      })
+    },
+    handleImport() {
+      this.$refs.cardImportFile && this.$refs.cardImportFile.click()
+    },
+    handleImportFileChange(event) {
+      const file = event.target.files && event.target.files[0]
+      if (!file) {
+        return
+      }
+      const formData = new FormData()
+      formData.append('file', file)
+      importCardData(formData).then(res => {
+        this.$modal.msgSuccess(getResponseData(res) || '导入成功')
+        this.getList()
+      }).finally(() => {
+        event.target.value = ''
+      })
+    },
+    handleDownloadTemplate() {
+      downloadCardTemplate().then(data => saveAs(new Blob([data]), '卡片导入模板.xlsx'))
+    },
+    handleExport() {
+      exportCard(this.queryParams).then(data => saveAs(new Blob([data]), '卡片列表.xlsx'))
+    }
+  }
+}
+</script>
+
+<style scoped>
+.card-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+}
+
+.card-import-file {
+  display: none;
+}
+
+.card-form-error {
+  color: #f56c6c;
+  line-height: 20px;
+  margin-top: 4px;
+}
+
+.danger-text {
+  color: #f56c6c;
+}
+</style>

--
Gitblit v1.7.1