liujie
2026-09-21 20051550f01227ecebbe0bf064e188dd242f58f2
update
17个文件已添加
9个文件已修改
1412 ■■■■■ 已修改文件
doc/sql/20260918_t_card.sql 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/sql/20260918_visitor_card_menu.sql 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysCardController.java 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-prod.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application-test.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCard.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDeleteRequest.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDetailRequest.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardEmployeeRequest.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardPageRequest.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardSaveRequest.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysCardMapper.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysEmployeeMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysCardService.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysCardServiceImpl.java 340 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysEmployeeServiceImpl.java 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/vo/SysCardVO.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysCardMapper.xml 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysEmployeeMapper.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysVisitorConfigMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/.env.production 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/dist.zip 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/api/visitor-admin/card.js 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/router/index.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/visitor-admin/card/index.vue 249 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/sql/20260918_t_card.sql
New file
@@ -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'
  );
doc/sql/20260918_visitor_card_menu.sql
New file
@@ -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`
  );
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/SysCardController.java
New file
@@ -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;
    }
}
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
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
ruoyi-admin/src/main/resources/application.yml
@@ -1,4 +1,4 @@
# 项目相关配置
spring:
  profiles:
    active: prod
    active: test
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysCard.java
New file
@@ -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;
}
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDeleteRequest.java
New file
@@ -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;
}
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardDetailRequest.java
New file
@@ -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;
}
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardEmployeeRequest.java
New file
@@ -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;
}
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardPageRequest.java
New file
@@ -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;
}
ruoyi-system/src/main/java/com/ruoyi/system/dto/SysCardSaveRequest.java
New file
@@ -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;
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysCardMapper.java
New file
@@ -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);
}
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);
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysCardService.java
New file
@@ -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);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysCardServiceImpl.java
New file
@@ -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)));
    }
}
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());
ruoyi-system/src/main/java/com/ruoyi/system/vo/SysCardVO.java
New file
@@ -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;
}
ruoyi-system/src/main/resources/mapper/system/SysCardMapper.xml
New file
@@ -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>
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,
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>
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'
ruoyi-ui/dist.zip
Binary files differ
ruoyi-ui/src/api/visitor-admin/card.js
New file
@@ -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' })
}
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' }
      }
    ]
  },
ruoyi-ui/src/views/visitor-admin/card/index.vue
New file
@@ -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>