Browse Source

1、初始化ruoyi-device模块
2、添加设备表tsb_device、tsb_user_device

liweimin 1 tháng trước cách đây
mục cha
commit
685e3acf8f

+ 8 - 0
pom.xml

@@ -170,6 +170,13 @@
                 <version>${ruoyi.version}</version>
             </dependency>
 
+            <!-- 设备模块-->
+            <dependency>
+                <groupId>com.ruoyi</groupId>
+                <artifactId>ruoyi-device</artifactId>
+                <version>${ruoyi.version}</version>
+            </dependency>
+
         </dependencies>
     </dependencyManagement>
 
@@ -180,6 +187,7 @@
         <module>ruoyi-quartz</module>
         <module>ruoyi-generator</module>
         <module>ruoyi-common</module>
+        <module>ruoyi-device</module>
     </modules>
     <packaging>pom</packaging>
 

+ 6 - 0
ruoyi-admin/pom.xml

@@ -54,6 +54,12 @@
             <artifactId>ruoyi-generator</artifactId>
         </dependency>
 
+        <!-- 设备模块-->
+        <dependency>
+            <groupId>com.ruoyi</groupId>
+            <artifactId>ruoyi-device</artifactId>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 210 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/device/TsbDeviceController.java

@@ -0,0 +1,210 @@
+package com.ruoyi.web.controller.device;
+
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.page.TableDataInfo;
+import com.ruoyi.common.enums.BusinessType;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.device.domain.entity.TsbDevice;
+import com.ruoyi.device.service.ITsbDeviceService;
+import com.ruoyi.device.service.ITsbUserDeviceService;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 调试宝设备信息
+ *
+ * @author lwm
+ */
+@RestController
+@RequestMapping("/tsb/device")
+public class TsbDeviceController extends BaseController
+{
+    @Autowired
+    private ITsbDeviceService tsbDeviceService;
+
+    @Autowired
+    private ITsbUserDeviceService tsbUserDeviceService;
+
+    /**
+     * 查询调试宝设备列表
+     */
+    @PreAuthorize("@ss.hasPermi('tsb:device:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(TsbDevice query)
+    {
+        startPage();
+        List<TsbDevice> list = tsbDeviceService.selectTsbDeviceList(query);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出调试宝设备列表
+     */
+    @Log(title = "调试宝设备", businessType = BusinessType.EXPORT)
+    @PreAuthorize("@ss.hasPermi('tsb:device:export')")
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, TsbDevice query)
+    {
+        List<TsbDevice> list = tsbDeviceService.selectTsbDeviceList(query);
+        ExcelUtil<TsbDevice> util = new ExcelUtil<>(TsbDevice.class);
+        util.exportExcel(response, list, "调试宝设备数据");
+    }
+
+    /**
+     * 根据设备ID获取详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('tsb:device:query')")
+    @GetMapping(value = "/{deviceId}")
+    public AjaxResult getInfo(@PathVariable Long deviceId)
+    {
+        return success(tsbDeviceService.selectTsbDeviceById(deviceId));
+    }
+
+    /**
+     * 新增调试宝设备
+     */
+    @PreAuthorize("@ss.hasPermi('tsb:device:add')")
+    @Log(title = "调试宝设备", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@Validated @RequestBody TsbDevice device)
+    {
+        if (!tsbDeviceService.checkImeiUnique(device))
+        {
+            return error("新增调试宝设备失败,IMEI已存在");
+        }
+        else if (!tsbDeviceService.checkDeviceSnUnique(device))
+        {
+            return error("新增调试宝设备失败,设备SN码已存在");
+        }
+        device.setCreateBy(getUsername());
+        return toAjax(tsbDeviceService.insertTsbDevice(device));
+    }
+
+    /**
+     * 修改调试宝设备
+     */
+    @PreAuthorize("@ss.hasPermi('tsb:device:edit')")
+    @Log(title = "调试宝设备", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@Validated @RequestBody TsbDevice device)
+    {
+        if (!tsbDeviceService.checkImeiUnique(device))
+        {
+            return error("修改调试宝设备失败,IMEI已存在");
+        }
+        else if (!tsbDeviceService.checkDeviceSnUnique(device))
+        {
+            return error("修改调试宝设备失败,设备SN码已存在");
+        }
+        device.setUpdateBy(getUsername());
+        return toAjax(tsbDeviceService.updateTsbDevice(device));
+    }
+
+    /**
+     * 状态修改(列表开关)
+     */
+    @PreAuthorize("@ss.hasPermi('tsb:device:edit')")
+    @Log(title = "调试宝设备", businessType = BusinessType.UPDATE)
+    @PutMapping("/changeStatus")
+    public AjaxResult changeStatus(@RequestBody TsbDevice device)
+    {
+        if (device.getDeviceId() == null)
+        {
+            return error("设备ID不能为空");
+        }
+        if (StringUtils.isEmpty(device.getStatus()))
+        {
+            return error("状态不能为空");
+        }
+        device.setUpdateBy(getUsername());
+        return toAjax(tsbDeviceService.updateTsbDeviceStatus(device));
+    }
+
+    /**
+     * 删除调试宝设备
+     */
+    @PreAuthorize("@ss.hasPermi('tsb:device:remove')")
+    @Log(title = "调试宝设备", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{deviceIds}")
+    public AjaxResult remove(@PathVariable Long[] deviceIds)
+    {
+        return toAjax(tsbDeviceService.deleteTsbDeviceByIds(deviceIds));
+    }
+
+    /**
+     * 强制删除调试宝设备(先解除用户与设备绑定,再逻辑删除设备)
+     */
+    @PreAuthorize("@ss.hasPermi('tsb:device:remove')")
+    @Log(title = "调试宝设备强制删除", businessType = BusinessType.DELETE)
+    @DeleteMapping("/force/{deviceIds}")
+    public AjaxResult forceRemove(@PathVariable Long[] deviceIds)
+    {
+        return toAjax(tsbDeviceService.forceDeleteTsbDeviceByIds(deviceIds));
+    }
+
+    /**
+     * 未绑定调试宝设备的用户(下拉框)
+     */
+    @PreAuthorize("@ss.hasPermi('tsb:device:list')")
+    @GetMapping("/unbindUsers")
+    public AjaxResult unbindUsers()
+    {
+        return success(tsbUserDeviceService.selectUnbindUsers());
+    }
+
+    /**
+     * 按设备ID查询当前绑定关系(无则 data 为 null)
+     */
+    @PreAuthorize("@ss.hasPermi('tsb:device:list')")
+    @GetMapping("/bindByDevice/{deviceId}")
+    public AjaxResult bindByDevice(@PathVariable Long deviceId)
+    {
+        return success(tsbUserDeviceService.selectBindByDeviceId(deviceId));
+    }
+
+    /**
+     * 用户绑定设备
+     */
+    @PreAuthorize("@ss.hasPermi('tsb:device:edit')")
+    @Log(title = "调试宝设备", businessType = BusinessType.GRANT)
+    @PutMapping("/bindUserDevice")
+    public AjaxResult bindUserDevice(Long userId, Long deviceId)
+    {
+        if (userId == null)
+        {
+            return error("用户ID不能为空");
+        }
+        if (deviceId == null)
+        {
+            return error("设备ID不能为空");
+        }
+        return toAjax(tsbUserDeviceService.insertUserDevice(userId, deviceId));
+    }
+
+    /**
+     * 用户解绑设备
+     */
+    @PreAuthorize("@ss.hasPermi('tsb:device:edit')")
+    @Log(title = "调试宝设备", businessType = BusinessType.GRANT)
+    @PutMapping("/unbindUserDevice")
+    public AjaxResult unbindUserDevice(Long userId, Long deviceId)
+    {
+        if (userId == null)
+        {
+            return error("用户ID不能为空");
+        }
+        if (deviceId == null)
+        {
+            return error("设备ID不能为空");
+        }
+        return toAjax(tsbUserDeviceService.deleteUserDevice(userId, deviceId));
+    }
+}

+ 31 - 4
ruoyi-admin/src/main/resources/application-druid.yml

@@ -6,9 +6,9 @@ spring:
         druid:
             # 主库数据源
             master:
-                url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
-                username: root
-                password: password
+                url: jdbc:mysql://192.168.0.100:3306/cpyypt-tsb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
+                username: wbjw
+                password: l1M9kX7S3z4RzcWW
             # 从库数据源
             slave:
                 # 从数据源开关/默认关闭
@@ -58,4 +58,31 @@ spring:
                     merge-sql: true
                 wall:
                     config:
-                        multi-statement-allow: true
+                        multi-statement-allow: true
+
+emqx:
+    broker: tcp://192.168.0.101:9000
+    userName: admin
+    password: houjianwei
+    cleanSession: true
+    reconnect: true
+    timeout: 20
+    keepAlive: 10
+    apiUserName: admin
+    apiPassword: public
+    apiPath: http://192.168.0.101:8081
+    downTopicTemplate: "cpyypt/down/%s/%s"
+    defaultFrameHeader: 0
+    messageRouters:
+        - topic: $share/wbjw/cpyypt/up/#
+          qos: 2
+          listener: deviceMessageListener
+        - topic: $share/wbjw/invent/up/#
+          qos: 2
+          listener: inventMessageListener
+        - topic: $SYS/brokers/+/clients/+/disconnected
+          qos: 2
+          listener: clientLineStatusListener
+        - topic: $share/wbjw/cpyypt/logup/#
+          qos: 2
+          listener: deviceLogListener

+ 3 - 3
ruoyi-admin/src/main/resources/application.yml

@@ -72,13 +72,13 @@ spring:
     # redis 配置
     redis:
       # 地址
-      host: localhost
+      host: 192.168.0.101
       # 端口,默认为6379
       port: 6379
       # 数据库索引
-      database: 0
+      database: 8
       # 密码
-      password:
+      password: weibaojinwang
       # 连接超时时间
       timeout: 10s
       lettuce:

+ 34 - 0
ruoyi-device/pom.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>ruoyi</artifactId>
+        <groupId>com.ruoyi</groupId>
+        <version>3.9.2</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ruoyi-device</artifactId>
+
+    <description>设备模块(调试宝等硬件接入)</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.ruoyi</groupId>
+            <artifactId>ruoyi-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.ruoyi</groupId>
+            <artifactId>ruoyi-system</artifactId>
+        </dependency>
+
+        <!-- 硬件 MQTT(EMQX / Eclipse Paho) -->
+        <dependency>
+            <groupId>org.eclipse.paho</groupId>
+            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+            <version>1.2.5</version>
+        </dependency>
+    </dependencies>
+
+</project>

+ 188 - 0
ruoyi-device/src/main/java/com/ruoyi/device/domain/entity/TsbDevice.java

@@ -0,0 +1,188 @@
+package com.ruoyi.device.domain.entity;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.common.annotation.Excel;
+import com.ruoyi.common.annotation.Excel.ColumnType;
+import com.ruoyi.common.annotation.Excel.Type;
+import com.ruoyi.common.core.domain.BaseEntity;
+
+import java.util.Date;
+
+/**
+ * 调试宝设备表 tsb_device
+ *
+ * @author lwm
+ */
+public class TsbDevice extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 设备ID */
+    @Excel(name = "设备序号", cellType = ColumnType.NUMERIC)
+    private Long deviceId;
+
+    /** 设备IMEI (唯一)*/
+    @Excel(name = "设备IMEI")
+    private String imei;
+
+    /** 设备类型 */
+    @Excel(name = "设备类型", readConverterExp = "9102=调试宝V3.0")
+    private String deviceType;
+
+    /** 设备SN码(在未删除记录范围内唯一) */
+    @Excel(name = "设备SN码", cellType = ColumnType.NUMERIC)
+    private Long deviceSn;
+
+    /** 设备生产日期 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    @Excel(name = "设备生产日期", width = 20, dateFormat = "yyyy-MM-dd")
+    private Date deviceProduceDate;
+
+    /** 软件版本号 */
+    @Excel(name = "软件版本号")
+    private String softwareVersion;
+
+    /** 状态(0正常 1停用) */
+    @Excel(name = "状态", readConverterExp = "0=正常,1=停用")
+    private String status;
+
+    /** 最后在线时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "最后在线时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT)
+    private Date lastRunTime;
+
+    /** 删除标志(0代表存在 2代表删除) */
+    private String delFlag;
+
+    public TsbDevice()
+    {
+
+    }
+
+    public TsbDevice(Long deviceId)
+    {
+        this.deviceId = deviceId;
+    }
+
+    public Long getDeviceId()
+    {
+        return deviceId;
+    }
+
+    public void setDeviceId(Long deviceId)
+    {
+        this.deviceId = deviceId;
+    }
+
+    @NotBlank(message = "IMEI不能为空")
+    @Size(max = 32, message = "IMEI长度不能超过32个字符")
+    public String getImei()
+    {
+        return imei;
+    }
+
+    public void setImei(String imei)
+    {
+        this.imei = imei;
+    }
+
+    @NotBlank(message = "设备型号不能为空")
+    @Size(max = 32, message = "设备型号长度不能超过32个字符")
+    public String getDeviceType()
+    {
+        return deviceType;
+    }
+
+    public void setDeviceType(String deviceType)
+    {
+        this.deviceType = deviceType;
+    }
+
+    @NotNull(message = "设备SN码不能为空")
+    public Long getDeviceSn()
+    {
+        return deviceSn;
+    }
+
+    public void setDeviceSn(Long deviceSn)
+    {
+        this.deviceSn = deviceSn;
+    }
+
+    public Date getDeviceProduceDate()
+    {
+        return deviceProduceDate;
+    }
+
+    public void setDeviceProduceDate(Date deviceProduceDate)
+    {
+        this.deviceProduceDate = deviceProduceDate;
+    }
+
+    @Size(max = 32, message = "软件版本号长度不能超过32个字符")
+    public String getSoftwareVersion()
+    {
+        return softwareVersion;
+    }
+
+    public void setSoftwareVersion(String softwareVersion)
+    {
+        this.softwareVersion = softwareVersion;
+    }
+
+    public String getStatus()
+    {
+        return status;
+    }
+
+    public void setStatus(String status)
+    {
+        this.status = status;
+    }
+
+    public Date getLastRunTime()
+    {
+        return lastRunTime;
+    }
+
+    public void setLastRunTime(Date lastRunTime)
+    {
+        this.lastRunTime = lastRunTime;
+    }
+
+    public String getDelFlag()
+    {
+        return delFlag;
+    }
+
+    public void setDelFlag(String delFlag)
+    {
+        this.delFlag = delFlag;
+    }
+
+    @Override
+    public String toString()
+    {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("deviceId", getDeviceId())
+            .append("imei", getImei())
+            .append("deviceType", getDeviceType())
+            .append("deviceSn", getDeviceSn())
+            .append("deviceProduceDate", getDeviceProduceDate())
+            .append("softwareVersion", getSoftwareVersion())
+            .append("status", getStatus())
+            .append("lastRunTime", getLastRunTime())
+            .append("delFlag", getDelFlag())
+            .append("createBy", getCreateBy())
+            .append("createTime", getCreateTime())
+            .append("updateBy", getUpdateBy())
+            .append("updateTime", getUpdateTime())
+            .append("remark", getRemark())
+            .toString();
+    }
+}

+ 68 - 0
ruoyi-device/src/main/java/com/ruoyi/device/domain/entity/TsbUserDevice.java

@@ -0,0 +1,68 @@
+package com.ruoyi.device.domain.entity;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 调试宝设备用户绑定 tsb_user_device
+ *
+ * @author lwm
+ */
+public class TsbUserDevice implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 用户ID */
+    private Long userId;
+
+    /** 调试宝设备ID */
+    private Long deviceId;
+
+    /** 绑定时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date bindTime;
+
+    public Long getUserId()
+    {
+        return userId;
+    }
+
+    public void setUserId(Long userId)
+    {
+        this.userId = userId;
+    }
+
+    public Long getDeviceId()
+    {
+        return deviceId;
+    }
+
+    public void setDeviceId(Long deviceId)
+    {
+        this.deviceId = deviceId;
+    }
+
+    public Date getBindTime()
+    {
+        return bindTime;
+    }
+
+    public void setBindTime(Date bindTime)
+    {
+        this.bindTime = bindTime;
+    }
+
+    @Override
+    public String toString()
+    {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+            .append("userId", getUserId())
+            .append("deviceId", getDeviceId())
+            .append("bindTime", getBindTime())
+            .toString();
+    }
+}

+ 85 - 0
ruoyi-device/src/main/java/com/ruoyi/device/domain/model/TsbUserDeviceBind.java

@@ -0,0 +1,85 @@
+package com.ruoyi.device.domain.model;
+
+import com.ruoyi.device.domain.entity.TsbUserDevice;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 设备与用户绑定关系信息封装
+ *
+ * @author lwm
+ */
+public class TsbUserDeviceBind extends TsbUserDevice
+{
+    private static final long serialVersionUID = 1L;
+
+    /** 登录账号 */
+    private String userName;
+
+    /** 用户昵称 */
+    private String nickName;
+
+    /** 设备IMEI */
+    private String imei;
+
+    /** 设备SN码 */
+    private Long deviceSn;
+
+    public TsbUserDeviceBind()
+    {
+    }
+
+    public String getUserName()
+    {
+        return userName;
+    }
+
+    public void setUserName(String userName)
+    {
+        this.userName = userName;
+    }
+
+    public String getNickName()
+    {
+        return nickName;
+    }
+
+    public void setNickName(String nickName)
+    {
+        this.nickName = nickName;
+    }
+
+    public String getImei()
+    {
+        return imei;
+    }
+
+    public void setImei(String imei)
+    {
+        this.imei = imei;
+    }
+
+    public Long getDeviceSn()
+    {
+        return deviceSn;
+    }
+
+    public void setDeviceSn(Long deviceSn)
+    {
+        this.deviceSn = deviceSn;
+    }
+
+    @Override
+    public String toString()
+    {
+        return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
+                .append("userId", getUserId())
+                .append("userName", getUserName())
+                .append("nickName", getNickName())
+                .append("deviceId", getDeviceId())
+                .append("imei", getImei())
+                .append("deviceSn", getDeviceSn())
+                .append("bindTime", getBindTime())
+                .toString();
+    }
+}

+ 87 - 0
ruoyi-device/src/main/java/com/ruoyi/device/mapper/TsbDeviceMapper.java

@@ -0,0 +1,87 @@
+package com.ruoyi.device.mapper;
+
+import com.ruoyi.device.domain.entity.TsbDevice;
+
+import java.util.List;
+
+/**
+ * 调试宝设备表 tsb_device 数据访问层
+ *
+ * @author lwm
+ */
+public interface TsbDeviceMapper
+{
+    /**
+     * 条件分页查询设备列表(仅 del_flag = 0)
+     * 条件含:deviceId、imei(模糊)、deviceType、deviceSn、status;
+     * 扩展请求参数:params.beginProduceDate、params.endProduceDate(yyyy-MM-dd,按 device_produce_date 日期闭区间)
+     *
+     * @param tsbDevice 查询条件,字段可为空;日期范围放在 BaseEntity.params 中
+     * @return 设备列表
+     */
+    List<TsbDevice> selectTsbDeviceList(TsbDevice tsbDevice);
+
+    /**
+     * 按主键查询未删除的设备
+     *
+     * @param deviceId 设备主键
+     * @return 设备实体,不存在或已逻辑删除时返回 null
+     */
+    TsbDevice selectTsbDeviceById(Long deviceId);
+
+    /**
+     * 按 IMEI 精确查询未删除的设备
+     *
+     * @param imei 设备 IMEI
+     * @return 设备实体
+     */
+    TsbDevice selectTsbDeviceByImei(String imei);
+
+    /**
+     * 按设备 SN 精确查询未删除的设备
+     *
+     * @param deviceSn 设备 SN 码
+     * @return 设备实体
+     */
+    TsbDevice selectTsbDeviceByDeviceSn(Long deviceSn);
+
+    /**
+     * IMEI 唯一性校验:在「未删除」设备中是否存在相同 IMEI
+     *
+     * @param imei     IMEI
+     * @return 设备信息
+     */
+    TsbDevice checkImeiUnique(String imei);
+
+    /**
+     * 设备 SN 唯一性校验:在「未删除」设备中是否存在相同 device_sn
+     *
+     * @param deviceSn 设备 SN 码
+     * @return 设备信息
+     */
+    TsbDevice checkDeviceSnUnique(Long deviceSn);
+
+    /**
+     * 新增设备,主键回填至实体 deviceId
+     *
+     * @param tsbDevice 设备实体
+     * @return 影响行数
+     */
+    int insertTsbDevice(TsbDevice tsbDevice);
+
+    /**
+     * 按主键更新设备(仅更新非空字段,且要求 del_flag = 0)
+     *
+     * @param tsbDevice 设备实体,须含 deviceId
+     * @return 影响行数
+     */
+    int updateTsbDevice(TsbDevice tsbDevice);
+
+    /**
+     * 批量逻辑删除:del_flag 置为 2
+     *
+     * @param deviceIds 设备主键数组
+     * @return 影响行数
+     */
+    int deleteTsbDeviceByIds(Long[] deviceIds);
+}

+ 89 - 0
ruoyi-device/src/main/java/com/ruoyi/device/mapper/TsbUserDeviceMapper.java

@@ -0,0 +1,89 @@
+package com.ruoyi.device.mapper;
+
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.device.domain.entity.TsbUserDevice;
+import com.ruoyi.device.domain.model.TsbUserDeviceBind;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 调试宝设备用户绑定Mapper
+ *
+ * @author lwm
+ */
+public interface TsbUserDeviceMapper
+{
+
+    /**
+     * 查询用户与设备绑定列表(联表用户、设备)
+     *
+     * @param userDeviceBind 查询参数
+     * @return 绑定关系集合信息
+     */
+    TsbUserDeviceBind selectTsbUserDeviceBind(TsbUserDeviceBind userDeviceBind);
+
+    /**
+     * 查询指定设备当前绑定
+     *
+     * @param deviceId 设备ID
+     * @return 绑定关系
+     */
+    TsbUserDeviceBind selectBindByDeviceId(Long deviceId);
+
+    /**
+     * 未绑定任何调试宝设备的用户列表(用于下拉)
+     *
+     * @return 用户列表
+     */
+    List<SysUser> selectUnbindUsers();
+
+    /**
+     * 通过用户id 查询设备绑定关系
+     *
+     * @param userId   用户ID
+     * @return 绑定关系
+     */
+    TsbUserDevice selectByUserId(Long userId);
+
+    /**
+     * 通过设备id 获取设备绑定关系
+     *
+     * @param deviceId 设备ID
+     * @return 绑定关系
+     */
+    TsbUserDevice selectByDeviceId(Long deviceId);
+
+    /**
+     * 用户与设备绑定
+     *
+     * @param bind 绑定关系
+     * @return 绑定结果
+     */
+    int insertTsbUserDevice(TsbUserDevice bind);
+
+    /**
+     * 用户与设备解绑
+     *
+     * @param userId   用户ID
+     * @param deviceId 设备ID
+     * @return 解绑结果
+     */
+    int deleteUserDevice(@Param("userId")Long userId, @Param("deviceId")Long deviceId);
+
+    /**
+     * 统计该设备的绑定条数(删除前校验用)
+     *
+     * @param deviceId 设备主键
+     * @return 绑定数量
+     */
+    int countBindByDeviceId(Long deviceId);
+
+    /**
+     * 按设备主键批量删除绑定(强制删设备前清理关联)
+     *
+     * @param deviceIds 设备主键数组
+     * @return 影响行数
+     */
+    int deleteTsbUserDeviceByDeviceIds(Long[] deviceIds);
+}

+ 85 - 0
ruoyi-device/src/main/java/com/ruoyi/device/service/ITsbDeviceService.java

@@ -0,0 +1,85 @@
+package com.ruoyi.device.service;
+
+import java.util.List;
+import com.ruoyi.device.domain.entity.TsbDevice;
+
+/**
+ * 调试宝设备业务层
+ *
+ * @author lwm
+ */
+public interface ITsbDeviceService
+{
+    /**
+     * 根据条件分页查询设备数据
+     *
+     * @param tsbDevice 设备信息(含查询条件及 params 中的生产日期区间等)
+     * @return 设备数据集合信息
+     */
+    public List<TsbDevice> selectTsbDeviceList(TsbDevice tsbDevice);
+
+    /**
+     * 根据设备ID查询设备(未删除)
+     *
+     * @param deviceId 设备ID
+     * @return 设备信息
+     */
+    public TsbDevice selectTsbDeviceById(Long deviceId);
+
+    /**
+     * 校验 IMEI 是否唯一(未删除的记录范围内)
+     *
+     * @param device 设备信息
+     * @return 结果
+     */
+    public boolean checkImeiUnique(TsbDevice device);
+
+    /**
+     * 校验设备 SN 是否唯一(未删除的记录范围内)
+     *
+     * @param device 设备信息
+     * @return 结果
+     */
+    public boolean checkDeviceSnUnique(TsbDevice device);
+
+    /**
+     * 新增调试宝设备
+     *
+     * @param device 设备信息
+     * @return 结果
+     */
+    public int insertTsbDevice(TsbDevice device);
+
+    /**
+     * 修改调试宝设备
+     *
+     * @param device 设备信息
+     * @return 结果
+     */
+    public int updateTsbDevice(TsbDevice device);
+
+    /**
+     * 修改设备状态(列表开关,仅需 deviceId、status)
+     *
+     * @param device 设备信息
+     * @return 结果
+     */
+    public int updateTsbDeviceStatus(TsbDevice device);
+
+    /**
+     * 批量逻辑删除设备
+     *
+     * @param deviceIds 需要删除的设备ID
+     * @return 结果
+     */
+    public int deleteTsbDeviceByIds(Long[] deviceIds);
+
+    /**
+     * 强制批量逻辑删除设备:先解除用户绑定,再删除设备
+     *
+     * @param deviceIds 需要删除的设备ID
+     * @return 结果
+     */
+    public int forceDeleteTsbDeviceByIds(Long[] deviceIds);
+
+}

+ 56 - 0
ruoyi-device/src/main/java/com/ruoyi/device/service/ITsbUserDeviceService.java

@@ -0,0 +1,56 @@
+package com.ruoyi.device.service;
+
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.device.domain.model.TsbUserDeviceBind;
+
+import java.util.List;
+
+/**
+ * 调试宝设备用户绑定业务层
+ *
+ * @author lwm
+ */
+public interface ITsbUserDeviceService
+{
+    /**
+     * 查询用户与设备绑定列表(联表用户、设备)
+     * 标识用户信息的 唯一字段至少存在一个、设备信息的 唯一字段至少存在一个
+     *
+     * @param userDeviceBind 查询参数
+     * @return 绑定关系集合信息
+     */
+    public TsbUserDeviceBind selectTsbUserDeviceBind(TsbUserDeviceBind userDeviceBind);
+
+    /**
+     * 查询指定设备当前绑定(无则返回 null)
+     *
+     * @param deviceId 设备ID
+     * @return 绑定关系
+     */
+    public TsbUserDeviceBind selectBindByDeviceId(Long deviceId);
+
+    /**
+     * 未绑定任何调试宝设备的用户列表(用于下拉)
+     *
+     * @return 用户列表
+     */
+    public List<SysUser> selectUnbindUsers();
+
+    /**
+     * 用户与设备绑定(一人一机、一机一人等规则在实现内校验)
+     *
+     * @param userId   用户ID
+     * @param deviceId 设备ID
+     * @return 结果
+     */
+    public int insertUserDevice(Long userId, Long deviceId);
+
+    /**
+     * 用户与设备解绑
+     *
+     * @param userId   用户ID
+     * @param deviceId 设备ID
+     * @return 结果
+     */
+    public int deleteUserDevice(Long userId, Long deviceId);
+}

+ 172 - 0
ruoyi-device/src/main/java/com/ruoyi/device/service/impl/TsbDeviceServiceImpl.java

@@ -0,0 +1,172 @@
+package com.ruoyi.device.service.impl;
+
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.device.domain.entity.TsbDevice;
+import com.ruoyi.device.mapper.TsbDeviceMapper;
+import com.ruoyi.device.mapper.TsbUserDeviceMapper;
+import com.ruoyi.device.service.ITsbDeviceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 调试宝设备 业务层处理
+ *
+ * @author lwm
+ */
+@Service
+public class TsbDeviceServiceImpl implements ITsbDeviceService
+{
+    @Autowired
+    private TsbDeviceMapper tsbDeviceMapper;
+
+    @Autowired
+    private TsbUserDeviceMapper tsbUserDeviceMapper;
+
+    /**
+     * 根据条件分页查询设备数据
+     *
+     * @param tsbDevice 设备信息
+     * @return 设备数据集合信息
+     */
+    @Override
+    public List<TsbDevice> selectTsbDeviceList(TsbDevice tsbDevice)
+    {
+        return tsbDeviceMapper.selectTsbDeviceList(tsbDevice);
+    }
+
+    /**
+     * 根据设备ID查询设备(未删除)
+     *
+     * @param deviceId 设备ID
+     * @return 设备信息
+     */
+    @Override
+    public TsbDevice selectTsbDeviceById(Long deviceId)
+    {
+        return tsbDeviceMapper.selectTsbDeviceById(deviceId);
+    }
+
+    /**
+     * 校验 IMEI 是否唯一
+     *
+     * @param device 设备信息
+     * @return 结果
+     */
+    @Override
+    public boolean checkImeiUnique(TsbDevice device)
+    {
+        Long deviceId = StringUtils.isNull(device.getDeviceId()) ? -1L : device.getDeviceId();
+        TsbDevice info = tsbDeviceMapper.checkImeiUnique(device.getImei());
+        if (StringUtils.isNotNull(info) && info.getDeviceId().longValue() != deviceId.longValue())
+        {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    /**
+     * 校验设备 SN 是否唯一
+     *
+     * @param device 设备信息
+     * @return 结果
+     */
+    @Override
+    public boolean checkDeviceSnUnique(TsbDevice device)
+    {
+        Long deviceId = StringUtils.isNull(device.getDeviceId()) ? -1L : device.getDeviceId();
+        TsbDevice info = tsbDeviceMapper.checkDeviceSnUnique(device.getDeviceSn());
+        if (StringUtils.isNotNull(info) && info.getDeviceId().longValue() != deviceId.longValue())
+        {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    /**
+     * 新增调试宝设备
+     *
+     * @param device 设备信息
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int insertTsbDevice(TsbDevice device)
+    {
+        if (StringUtils.isEmpty(device.getStatus()))
+        {
+            device.setStatus(UserConstants.NORMAL);
+        }
+        return tsbDeviceMapper.insertTsbDevice(device);
+    }
+
+    /**
+     * 修改调试宝设备
+     *
+     * @param device 设备信息
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int updateTsbDevice(TsbDevice device)
+    {
+        return tsbDeviceMapper.updateTsbDevice(device);
+    }
+
+    /**
+     * 修改设备状态
+     *
+     * @param device 设备信息(deviceId、status)
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int updateTsbDeviceStatus(TsbDevice device)
+    {
+        return tsbDeviceMapper.updateTsbDevice(device);
+    }
+
+    /**
+     * 批量逻辑删除设备;若仍有用户绑定则不允许删除
+     *
+     * @param deviceIds 需要删除的设备ID
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int deleteTsbDeviceByIds(Long[] deviceIds)
+    {
+        for (Long deviceId : deviceIds)
+        {
+            if (tsbUserDeviceMapper.countBindByDeviceId(deviceId) > 0)
+            {
+                TsbDevice d = tsbDeviceMapper.selectTsbDeviceById(deviceId);
+                String tip = StringUtils.isNotNull(d) ? String.valueOf(d.getDeviceSn()) : String.valueOf(deviceId);
+                throw new ServiceException(String.format("设备[%s]已绑定用户,请先解绑", tip));
+            }
+        }
+        return tsbDeviceMapper.deleteTsbDeviceByIds(deviceIds);
+    }
+
+    /**
+     * 强制删除:先删绑定,再逻辑删设备
+     *
+     * @param deviceIds 设备主键数组
+     * @return 影响行数(以设备表为准)
+     */
+    @Override
+    @Transactional
+    public int forceDeleteTsbDeviceByIds(Long[] deviceIds)
+    {
+        if (StringUtils.isEmpty(deviceIds))
+        {
+            return 0;
+        }
+        tsbUserDeviceMapper.deleteTsbUserDeviceByDeviceIds(deviceIds);
+        return tsbDeviceMapper.deleteTsbDeviceByIds(deviceIds);
+    }
+}

+ 131 - 0
ruoyi-device/src/main/java/com/ruoyi/device/service/impl/TsbUserDeviceServiceImpl.java

@@ -0,0 +1,131 @@
+package com.ruoyi.device.service.impl;
+
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.enums.UserStatus;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.device.domain.entity.TsbDevice;
+import com.ruoyi.device.domain.entity.TsbUserDevice;
+import com.ruoyi.device.domain.model.TsbUserDeviceBind;
+import com.ruoyi.device.mapper.TsbDeviceMapper;
+import com.ruoyi.device.mapper.TsbUserDeviceMapper;
+import com.ruoyi.device.service.ITsbUserDeviceService;
+import com.ruoyi.system.mapper.SysUserMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * 调试宝设备用户绑定 业务层处理
+ *
+ * @author lwm
+ */
+@Service
+public class TsbUserDeviceServiceImpl implements ITsbUserDeviceService
+{
+    @Autowired
+    private TsbUserDeviceMapper tsbUserDeviceMapper;
+
+    @Autowired
+    private TsbDeviceMapper tsbDeviceMapper;
+
+    @Autowired
+    private SysUserMapper userMapper;
+
+    /**
+     * 查询用户与设备绑定列表(联表用户、设备)
+     *
+     * @param userDeviceBind 查询参数
+     * @return 绑定关系集合信息
+     */
+    @Override
+    public TsbUserDeviceBind selectTsbUserDeviceBind(TsbUserDeviceBind userDeviceBind)
+    {
+        if (userDeviceBind == null) {
+            return null;
+        }
+        if (userDeviceBind.getUserId() == null && StringUtils.isEmpty(userDeviceBind.getUserName()) && StringUtils.isEmpty(userDeviceBind.getNickName())) {
+            return null;
+        }
+        if (userDeviceBind.getDeviceId() == null && StringUtils.isEmpty(userDeviceBind.getImei()) && userDeviceBind.getDeviceSn() == null) {
+            return null;
+        }
+        return tsbUserDeviceMapper.selectTsbUserDeviceBind(userDeviceBind);
+    }
+
+    /**
+     * 查询指定设备当前绑定
+     *
+     * @param deviceId 设备ID
+     * @return 绑定关系
+     */
+    @Override
+    public TsbUserDeviceBind selectBindByDeviceId(Long deviceId)
+    {
+        return tsbUserDeviceMapper.selectBindByDeviceId(deviceId);
+    }
+
+    /**
+     * 未绑定任何调试宝设备的用户列表(用于下拉)
+     *
+     * @return 用户列表
+     */
+    @Override
+    public List<SysUser> selectUnbindUsers()
+    {
+        return tsbUserDeviceMapper.selectUnbindUsers();
+    }
+
+    /**
+     * 用户与设备绑定
+     *
+     * @param userId   用户ID
+     * @param deviceId 设备ID
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int insertUserDevice(Long userId, Long deviceId)
+    {
+        SysUser user = userMapper.selectUserById(userId);
+        if (user == null || UserStatus.DELETED.getCode().equals(user.getDelFlag()))
+        {
+            throw new ServiceException("用户不存在或已删除");
+        }
+        TsbDevice device = tsbDeviceMapper.selectTsbDeviceById(deviceId);
+        if (device == null)
+        {
+            throw new ServiceException("设备不存在或已删除");
+        }
+        if (tsbUserDeviceMapper.selectByUserId(userId) != null)
+        {
+            throw new ServiceException("该用户已绑定设备,请先解绑");
+        }
+        if (tsbUserDeviceMapper.selectByDeviceId(deviceId) != null)
+        {
+            throw new ServiceException("该设备已被其他用户绑定");
+        }
+        TsbUserDevice row = new TsbUserDevice();
+        row.setUserId(userId);
+        row.setDeviceId(deviceId);
+        row.setBindTime(DateUtils.getNowDate());
+        return tsbUserDeviceMapper.insertTsbUserDevice(row);
+    }
+
+    /**
+     * 用户与设备解绑
+     *
+     * @param userId   用户ID
+     * @param deviceId 设备ID
+     * @return 结果
+     */
+    @Override
+    @Transactional
+    public int deleteUserDevice(Long userId, Long deviceId)
+    {
+        return tsbUserDeviceMapper.deleteUserDevice(userId, deviceId);
+    }
+}

+ 138 - 0
ruoyi-device/src/main/resources/mapper/device/TsbDeviceMapper.xml

@@ -0,0 +1,138 @@
+<?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.device.mapper.TsbDeviceMapper">
+
+    <resultMap type="TsbDevice" id="TsbDeviceResult">
+        <id     property="deviceId"           column="device_id" />
+        <result property="imei"               column="imei" />
+        <result property="deviceType"         column="device_type" />
+        <result property="deviceSn"           column="device_sn" />
+        <result property="deviceProduceDate"  column="device_produce_date" />
+        <result property="softwareVersion"    column="software_version" />
+        <result property="status"             column="status" />
+        <result property="lastRunTime"        column="last_run_time" />
+        <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" />
+        <result property="remark"             column="remark" />
+    </resultMap>
+
+    <sql id="selectTsbDeviceVo">
+        select device_id, imei, device_type, device_sn, device_produce_date, software_version,
+               status, last_run_time, del_flag, create_by, create_time, update_by, update_time, remark
+        from tsb_device
+    </sql>
+
+    <select id="selectTsbDeviceList" parameterType="TsbDevice" resultMap="TsbDeviceResult">
+        <include refid="selectTsbDeviceVo"/>
+        <where>
+            and del_flag = '0'
+            <if test="deviceId != null and deviceId != 0">
+                AND device_id = #{deviceId}
+            </if>
+            <if test="imei != null and imei != ''">
+                AND imei like concat('%', #{imei}, '%')
+            </if>
+            <if test="deviceType != null and deviceType != ''">
+                AND device_type = #{deviceType}
+            </if>
+            <if test="deviceSn != null">
+                AND device_sn = #{deviceSn}
+            </if>
+            <if test="status != null and status != ''">
+                AND status = #{status}
+            </if>
+            <if test="params.beginProduceDate != null and params.beginProduceDate != ''">
+                AND date_format(device_produce_date,'%Y%m%d') &gt;= date_format(#{params.beginProduceDate},'%Y%m%d')
+            </if>
+            <if test="params.endProduceDate != null and params.endProduceDate != ''">
+                AND date_format(device_produce_date,'%Y%m%d') &lt;= date_format(#{params.endProduceDate},'%Y%m%d')
+            </if>
+        </where>
+        order by device_id desc
+    </select>
+
+    <select id="selectTsbDeviceById" parameterType="Long" resultMap="TsbDeviceResult">
+        <include refid="selectTsbDeviceVo"/>
+        where device_id = #{deviceId} and del_flag = '0'
+    </select>
+
+    <select id="selectTsbDeviceByImei" parameterType="String" resultMap="TsbDeviceResult">
+        <include refid="selectTsbDeviceVo"/>
+        where imei = #{imei} and del_flag = '0'
+    </select>
+
+    <select id="selectTsbDeviceByDeviceSn" parameterType="Long" resultMap="TsbDeviceResult">
+        <include refid="selectTsbDeviceVo"/>
+        where device_sn = #{deviceSn} and del_flag = '0'
+    </select>
+
+    <select id="checkImeiUnique" parameterType="String" resultMap="TsbDeviceResult">
+        <include refid="selectTsbDeviceVo"/>
+        where imei = #{imei} and del_flag = '0' limit 1
+    </select>
+
+    <select id="checkDeviceSnUnique" parameterType="Long" resultMap="TsbDeviceResult">
+        <include refid="selectTsbDeviceVo"/>
+        where device_sn = #{deviceSn} and del_flag = '0' limit 1
+    </select>
+
+    <insert id="insertTsbDevice" parameterType="TsbDevice" useGeneratedKeys="true" keyProperty="deviceId">
+        insert into tsb_device(
+            <if test="deviceId != null and deviceId != 0">device_id,</if>
+            <if test="imei != null and imei != ''">imei,</if>
+            <if test="deviceType != null and deviceType != ''">device_type,</if>
+            <if test="deviceSn != null">device_sn,</if>
+            <if test="deviceProduceDate != null">device_produce_date,</if>
+            <if test="softwareVersion != null and softwareVersion != ''">software_version,</if>
+            <if test="status != null and status != ''">status,</if>
+            <if test="lastRunTime != null">last_run_time,</if>
+            <if test="createBy != null and createBy != ''">create_by,</if>
+            <if test="remark != null and remark != ''">remark,</if>
+            create_time
+        )values(
+            <if test="deviceId != null and deviceId != 0">#{deviceId},</if>
+            <if test="imei != null and imei != ''">#{imei},</if>
+            <if test="deviceType != null and deviceType != ''">#{deviceType},</if>
+            <if test="deviceSn != null">#{deviceSn},</if>
+            <if test="deviceProduceDate != null">#{deviceProduceDate},</if>
+            <if test="softwareVersion != null and softwareVersion != ''">#{softwareVersion},</if>
+            <if test="status != null and status != ''">#{status},</if>
+            <if test="lastRunTime != null">#{lastRunTime},</if>
+            <if test="createBy != null and createBy != ''">#{createBy},</if>
+            <if test="remark != null and remark != ''">#{remark},</if>
+            sysdate()
+        )
+    </insert>
+
+    <update id="updateTsbDevice" parameterType="TsbDevice">
+        update tsb_device
+        <set>
+            <if test="imei != null and imei != ''">imei = #{imei},</if>
+            <if test="deviceType != null and deviceType != ''">device_type = #{deviceType},</if>
+            <if test="deviceSn != null">device_sn = #{deviceSn},</if>
+            <if test="deviceProduceDate != null">device_produce_date = #{deviceProduceDate},</if>
+            <if test="softwareVersion != null and softwareVersion != ''">software_version = #{softwareVersion},</if>
+            <if test="status != null and status != ''">status = #{status},</if>
+            <if test="lastRunTime != null">last_run_time = #{lastRunTime},</if>
+            <if test="remark != null and remark != ''">remark = #{remark},</if>
+            <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+            update_time = sysdate()
+        </set>
+        where device_id = #{deviceId} and del_flag = '0'
+    </update>
+
+    <update id="deleteTsbDeviceByIds">
+        update tsb_device set del_flag = '2', update_time = sysdate()
+        where device_id in
+        <foreach collection="array" item="deviceId" open="(" separator="," close=")">
+            #{deviceId}
+        </foreach>
+        and del_flag = '0'
+    </update>
+
+</mapper>

+ 111 - 0
ruoyi-device/src/main/resources/mapper/device/TsbUserDeviceMapper.xml

@@ -0,0 +1,111 @@
+<?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.device.mapper.TsbUserDeviceMapper">
+
+    <resultMap type="TsbUserDevice" id="TsbUserDeviceResult">
+        <result property="userId"      column="user_id" />
+        <result property="deviceId"    column="device_id" />
+        <result property="bindTime"    column="bind_time" />
+    </resultMap>
+
+    <select id="selectTsbUserDeviceBind" parameterType="TsbUserDeviceBind" resultType="TsbUserDeviceBind">
+        select bud.user_id as userId,
+                u.user_name as userName,
+                u.nick_name as nickName,
+                bud.device_id as deviceId,
+                d.imei as imei,
+                d.device_sn as deviceSn,
+                bud.bind_time as bindTime
+        from tsb_user_device bud
+                left join sys_user u on u.user_id = bud.user_id and u.del_flag = '0'
+                left join tsb_device d on d.device_id = bud.device_id and d.del_flag = '0'
+        <where>
+            <if test="userId != null">
+                and bud.user_id = #{userId}
+            </if>
+            <if test="userName != null and userName != ''">
+                and u.user_name = #{userName}
+            </if>
+            <if test="nickName != null and nickName != ''">
+                and u.nick_name = #{nickName}
+            </if>
+            <if test="deviceId != null">
+                and bud.device_id = #{deviceId}
+            </if>
+            <if test="imei != null and imei != ''">
+                and d.imei = #{imei}
+            </if>
+            <if test="deviceSn != null">
+                and d.device_sn = #{deviceSn}
+            </if>
+        </where>
+        order by bud.device_id desc, bud.user_id desc limit 1
+    </select>
+
+    <select id="selectBindByDeviceId" parameterType="Long" resultType="TsbUserDeviceBind">
+        select bud.user_id as userId,
+               u.user_name as userName,
+               u.nick_name as nickName,
+               bud.device_id as deviceId,
+               d.imei as imei,
+               d.device_sn as deviceSn,
+               bud.bind_time as bindTime
+        from tsb_user_device bud
+                 left join sys_user u on u.user_id = bud.user_id and u.del_flag = '0'
+                 left join tsb_device d on d.device_id = bud.device_id and d.del_flag = '0'
+        where bud.device_id = #{deviceId}
+        order by bud.user_id desc limit 1
+    </select>
+
+    <select id="selectUnbindUsers" resultType="SysUser">
+        select u.user_id   as userId,
+               u.user_name as userName,
+               u.nick_name as nickName
+        from sys_user u
+        where u.del_flag = '0'
+          and u.status = '0'
+          and not exists (select 1 from tsb_user_device bud where bud.user_id = u.user_id)
+        order by u.user_id
+    </select>
+
+    <select id="selectByUserId" parameterType="Long" resultMap="TsbUserDeviceResult">
+        select user_id, device_id, bind_time
+        from tsb_user_device
+        where user_id = #{userId}
+        limit 1
+    </select>
+
+    <select id="selectByDeviceId" parameterType="Long" resultMap="TsbUserDeviceResult">
+        select user_id, device_id, bind_time
+        from tsb_user_device
+        where device_id = #{deviceId}
+        limit 1
+    </select>
+
+    <insert id="insertTsbUserDevice" parameterType="TsbUserDevice">
+        insert into tsb_user_device(user_id, device_id, bind_time)
+        values (#{userId}, #{deviceId},
+            <choose>
+                <when test="bindTime != null">#{bindTime}</when>
+                <otherwise>sysdate()</otherwise>
+            </choose>
+        )
+    </insert>
+
+    <delete id="deleteUserDevice" parameterType="Long">
+        delete from tsb_user_device where user_id = #{userId} and device_id = #{deviceId}
+    </delete>
+
+    <select id="countBindByDeviceId" parameterType="Long" resultType="int">
+        select count(1) from tsb_user_device where device_id = #{deviceId}
+    </select>
+
+    <delete id="deleteTsbUserDeviceByDeviceIds">
+        delete from tsb_user_device where device_id in
+        <foreach collection="array" item="deviceId" open="(" separator="," close=")">
+            #{deviceId}
+        </foreach>
+    </delete>
+</mapper>

+ 93 - 0
ruoyi-ui/src/api/tsb/device.js

@@ -0,0 +1,93 @@
+import request from '@/utils/request'
+
+export function listTsbDevice(query) {
+  return request({
+    url: '/tsb/device/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getTsbDevice(deviceId) {
+  return request({
+    url: '/tsb/device/' + deviceId,
+    method: 'get'
+  })
+}
+
+export function addTsbDevice(data) {
+  return request({
+    url: '/tsb/device',
+    method: 'post',
+    data: data
+  })
+}
+
+export function updateTsbDevice(data) {
+  return request({
+    url: '/tsb/device',
+    method: 'put',
+    data: data
+  })
+}
+
+export function changeTsbDeviceStatus(deviceId, status) {
+  return request({
+    url: '/tsb/device/changeStatus',
+    method: 'put',
+    data: { deviceId, status }
+  })
+}
+
+export function delTsbDevice(deviceId) {
+  return request({
+    url: '/tsb/device/' + deviceId,
+    method: 'delete'
+  })
+}
+
+/** 强制删除:先解绑再逻辑删设备 */
+export function forceDelTsbDevice(deviceIds) {
+  return request({
+    url: '/tsb/device/force/' + deviceIds,
+    method: 'delete'
+  })
+}
+
+export function listTsbBind(query) {
+  return request({
+    url: '/tsb/device/bind/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function listTsbUnboundUsers() {
+  return request({
+    url: '/tsb/device/unbindUsers',
+    method: 'get'
+  })
+}
+
+export function getTsbBindByDevice(deviceId) {
+  return request({
+    url: '/tsb/device/bindByDevice/' + deviceId,
+    method: 'get'
+  })
+}
+
+export function bindTsbUserDevice(userId, deviceId) {
+  return request({
+    url: '/tsb/device/bindUserDevice',
+    method: 'put',
+    params: { userId, deviceId }
+  })
+}
+
+export function unbindTsbUserDevice(userId, deviceId) {
+  return request({
+    url: '/tsb/device/unbindUserDevice',
+    method: 'put',
+    params: { userId, deviceId }
+  })
+}

+ 462 - 0
ruoyi-ui/src/views/tsb/device/index.vue

@@ -0,0 +1,462 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="IMEI" prop="imei">
+        <el-input v-model="queryParams.imei" placeholder="IMEI" clearable @keyup.enter.native="handleQueryDevice" />
+      </el-form-item>
+      <el-form-item label="型号" prop="deviceType">
+        <el-input v-model="queryParams.deviceType" placeholder="设备型号" clearable @keyup.enter.native="handleQueryDevice" />
+      </el-form-item>
+      <el-form-item label="设备SN" prop="deviceSn">
+        <el-input-number
+          v-model="queryParams.deviceSn"
+          :controls="false"
+          :min="0"
+          placeholder="设备SN"
+          style="width: 160px"
+          @keyup.enter.native="handleQueryDevice"
+        />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="状态" clearable>
+          <el-option v-for="dict in dict.type.sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="生产日期">
+        <el-date-picker
+          v-model="produceDateRange"
+          style="width: 240px"
+          value-format="yyyy-MM-dd"
+          type="daterange"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQueryDevice">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQueryDevice">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddDevice" v-hasPermi="['tsb:device:add']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="deviceSingle" @click="handleUpdateDevice()" v-hasPermi="['tsb:device:edit']">修改</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="deviceMultiple" @click="handleDeleteDevice" v-hasPermi="['tsb:device:remove']">删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="el-icon-warning-outline" size="mini" :disabled="deviceMultiple" @click="handleForceDeleteDevice" v-hasPermi="['tsb:device:remove']">强制删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExportDevice"
+          v-hasPermi="['tsb:device:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getDeviceList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="deviceLoading" :data="deviceList" @selection-change="handleDeviceSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="设备ID" align="center" prop="deviceId" width="80" />
+      <el-table-column label="IMEI" align="center" prop="imei" width="150" show-overflow-tooltip />
+      <el-table-column label="型号" align="center" prop="deviceType" show-overflow-tooltip />
+      <el-table-column label="SN" align="center" prop="deviceSn" />
+      <el-table-column label="生产日期" align="center" prop="deviceProduceDate" width="120">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.deviceProduceDate, '{y}-{m}-{d}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="软件版本" align="center" prop="softwareVersion" width="100" />
+      <el-table-column label="状态" align="center" prop="status" width="100">
+        <template slot-scope="scope">
+          <el-switch
+            v-model="scope.row.status"
+            active-value="0"
+            inactive-value="1"
+            @change="handleDeviceStatusChange(scope.row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="最后在线时间" align="center" prop="lastRunTime" width="160">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.lastRunTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="340">
+        <template slot-scope="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdateDevice(scope.row)" v-hasPermi="['tsb:device:edit']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDeleteDevice(scope.row)" v-hasPermi="['tsb:device:remove']">删除</el-button>
+          <el-button size="mini" type="text" icon="el-icon-warning-outline" @click="handleForceDeleteDevice(scope.row)" v-hasPermi="['tsb:device:remove']">强制删除</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-user"
+            @click="openDeviceBindUser(scope.row)"
+            v-hasPermi="['tsb:device:edit']"
+          >设备绑定用户</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination v-show="deviceTotal > 0" :total="deviceTotal" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDeviceList" />
+
+    <el-dialog :title="deviceTitle" :visible.sync="deviceOpen" width="560px" append-to-body>
+      <el-form ref="deviceForm" :model="deviceForm" :rules="deviceRules" label-width="100px">
+        <el-form-item label="IMEI" prop="imei">
+          <el-input v-model="deviceForm.imei" placeholder="IMEI" maxlength="32" />
+        </el-form-item>
+        <el-form-item label="设备型号" prop="deviceType">
+          <el-input v-model="deviceForm.deviceType" placeholder="设备型号" maxlength="32" />
+        </el-form-item>
+        <el-form-item label="设备SN" prop="deviceSn">
+          <el-input-number v-model="deviceForm.deviceSn" :controls="false" placeholder="SN" style="width:100%" />
+        </el-form-item>
+        <el-form-item label="生产日期" prop="deviceProduceDate">
+          <el-date-picker
+            v-model="deviceForm.deviceProduceDate"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择生产日期"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="软件版本" prop="softwareVersion">
+          <el-input v-model="deviceForm.softwareVersion" placeholder="软件版本" maxlength="32" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-radio-group v-model="deviceForm.status">
+            <el-radio v-for="dict in dict.type.sys_normal_disable" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="deviceForm.remark" type="textarea" placeholder="备注" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitDeviceForm">确 定</el-button>
+        <el-button @click="deviceOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="设备用户绑定" :visible.sync="deviceBindOpen" width="520px" append-to-body @open="onDeviceBindDialogOpen">
+      <div v-if="bindScopeDevice" style="margin-bottom: 12px">
+        <p><b>设备IMEI:</b>{{ bindScopeDevice.imei }}</p>
+        <p><b>设备SN:</b>{{ bindScopeDevice.deviceSn }}</p>
+      </div>
+      <el-divider content-position="left">当前绑定</el-divider>
+      <div v-if="currentDeviceBind" style="margin-bottom: 12px">
+        <span>{{ currentDeviceBind.userName }}({{ currentDeviceBind.nickName || '-' }})</span>
+        <span style="margin-left: 12px; color: #606266">
+          绑定时间:{{ currentDeviceBind.bindTime ? parseTime(currentDeviceBind.bindTime) : '—' }}
+        </span>
+        <el-button
+          type="text"
+          icon="el-icon-close"
+          style="margin-left: 8px"
+          @click="handleUnbindCurrentUser"
+          v-hasPermi="['tsb:device:edit']"
+        >解绑</el-button>
+      </div>
+      <div v-else style="color: #909399; margin-bottom: 12px">暂无绑定用户</div>
+      <el-divider content-position="left">绑定新用户</el-divider>
+      <el-form label-width="100px">
+        <el-form-item label="选择用户">
+          <el-select
+            v-model="bindSelectedUserId"
+            placeholder="请选择未绑定设备的用户"
+            filterable
+            clearable
+            style="width: 100%"
+            :disabled="!!currentDeviceBind"
+          >
+            <el-option
+              v-for="u in unboundUserOptions"
+              :key="u.userId"
+              :label="u.userName + (u.nickName ? '(' + u.nickName + ')' : '')"
+              :value="u.userId"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitDeviceBind" v-hasPermi="['tsb:device:edit']" :disabled="!!currentDeviceBind">确 定绑定</el-button>
+        <el-button @click="deviceBindOpen = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listTsbDevice,
+  getTsbDevice,
+  addTsbDevice,
+  updateTsbDevice,
+  changeTsbDeviceStatus,
+  delTsbDevice,
+  forceDelTsbDevice,
+  listTsbUnboundUsers,
+  getTsbBindByDevice,
+  bindTsbUserDevice,
+  unbindTsbUserDevice
+} from '@/api/tsb/device'
+
+export default {
+  name: 'TsbDevice',
+  dicts: ['sys_normal_disable'],
+  data() {
+    return {
+      showSearch: true,
+      deviceLoading: true,
+      deviceList: [],
+      deviceIds: [],
+      deviceSingle: true,
+      deviceMultiple: true,
+      deviceTotal: 0,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        imei: undefined,
+        deviceType: undefined,
+        deviceSn: undefined,
+        status: undefined
+      },
+      produceDateRange: [],
+      deviceOpen: false,
+      deviceTitle: '',
+      deviceForm: {},
+      deviceRules: {
+        imei: [{ required: true, message: 'IMEI不能为空', trigger: 'blur' }]
+      },
+      deviceBindOpen: false,
+      bindScopeDevice: null,
+      currentDeviceBind: null,
+      unboundUserOptions: [],
+      bindSelectedUserId: undefined
+    }
+  },
+  created() {
+    this.getDeviceList()
+  },
+  methods: {
+    getDeviceList() {
+      this.deviceLoading = true
+      listTsbDevice(this.addDateRange(this.queryParams, this.produceDateRange, 'ProduceDate')).then(response => {
+        this.deviceList = response.rows
+        this.deviceTotal = response.total
+        this.deviceLoading = false
+      })
+    },
+    handleQueryDevice() {
+      this.queryParams.pageNum = 1
+      this.getDeviceList()
+    },
+    resetQueryDevice() {
+      this.produceDateRange = []
+      this.resetForm('queryForm')
+      this.handleQueryDevice()
+    },
+    /** 导出设备列表(与当前查询条件、生产日期区间一致) */
+    handleExportDevice() {
+      this.download(
+        'tsb/device/export',
+        this.addDateRange({ ...this.queryParams }, this.produceDateRange, 'ProduceDate'),
+        `tsb_device_${new Date().getTime()}.xlsx`
+      )
+    },
+    handleDeviceSelectionChange(selection) {
+      this.deviceIds = selection.map(item => item.deviceId)
+      this.deviceSingle = selection.length !== 1
+      this.deviceMultiple = !selection.length
+    },
+    handleAddDevice() {
+      this.resetDeviceForm()
+      this.deviceOpen = true
+      this.deviceTitle = '新增设备'
+    },
+    handleUpdateDevice(row) {
+      const deviceId = row && row.deviceId ? row.deviceId : this.deviceIds[0]
+      this.resetDeviceForm()
+      getTsbDevice(deviceId).then(response => {
+        this.deviceForm = response.data
+        this.deviceOpen = true
+        this.deviceTitle = '修改设备'
+      })
+    },
+    submitDeviceForm() {
+      this.$refs['deviceForm'].validate(valid => {
+        if (!valid) return
+        if (this.deviceForm.deviceId != null) {
+          updateTsbDevice(this.deviceForm).then(() => {
+            this.$modal.msgSuccess('修改成功')
+            this.deviceOpen = false
+            this.getDeviceList()
+          })
+        } else {
+          addTsbDevice(this.deviceForm).then(() => {
+            this.$modal.msgSuccess('新增成功')
+            this.deviceOpen = false
+            this.getDeviceList()
+          })
+        }
+      })
+    },
+    /** 删除确认文案:以设备 SN 为准,无 SN 时用设备 ID */
+    deviceSnDeleteLabel(row) {
+      if (row && row.deviceId != null) {
+        if (row.deviceSn != null && row.deviceSn !== '') {
+          return String(row.deviceSn)
+        }
+        return 'ID:' + row.deviceId
+      }
+      if (!this.deviceIds || this.deviceIds.length === 0) {
+        return ''
+      }
+      return this.deviceIds.map(id => {
+        const d = this.deviceList.find(x => x.deviceId === id)
+        if (d && d.deviceSn != null && d.deviceSn !== '') {
+          return String(d.deviceSn)
+        }
+        return 'ID:' + id
+      }).join('、')
+    },
+    extractErrorMessage(err) {
+      if (typeof err === 'string') {
+        return err
+      }
+      if (err && err.message) {
+        return err.message
+      }
+      return ''
+    },
+    /** 普通 remove 因绑定失败时的后端提示 */
+    isBindUserBlockDeleteError(msg) {
+      return typeof msg === 'string' && msg.indexOf('已绑定用户') !== -1 && msg.indexOf('请先解绑') !== -1
+    },
+    handleDeleteDevice(row) {
+      const ids = row && row.deviceId ? row.deviceId : this.deviceIds
+      const snLabel = this.deviceSnDeleteLabel(row)
+      this.$modal.confirm('是否确认删除设备SN为"' + snLabel + '"的数据项?').then(() => {
+        return delTsbDevice(ids)
+      }).then(() => {
+        this.getDeviceList()
+        this.$modal.msgSuccess('删除成功')
+      }).catch(err => {
+        const msg = this.extractErrorMessage(err)
+        if (this.isBindUserBlockDeleteError(msg)) {
+          this.$modal.confirm('该设备已绑定用户。是否改为强制删除(将自动解除绑定并删除设备)?').then(() => {
+            return forceDelTsbDevice(ids)
+          }).then(() => {
+            this.getDeviceList()
+            this.$modal.msgSuccess('强制删除成功')
+          }).catch(() => {})
+        }
+      })
+    },
+    handleForceDeleteDevice(row) {
+      const ids = row && row.deviceId ? row.deviceId : this.deviceIds
+      const snLabel = this.deviceSnDeleteLabel(row)
+      this.$modal.confirm(
+        '强制删除将解除这些设备与用户的绑定关系,并删除设备。是否确认强制删除设备SN为"' + snLabel + '"?'
+      ).then(() => {
+        return forceDelTsbDevice(ids)
+      }).then(() => {
+        this.getDeviceList()
+        this.$modal.msgSuccess('强制删除成功')
+      }).catch(() => {})
+    },
+    handleDeviceStatusChange(row) {
+      const text = row.status === '0' ? '启用' : '停用'
+      const snLabel = this.deviceSnDeleteLabel(row)
+      this.$modal.confirm('确认要"' + text + '""' + snLabel + '"设备吗?').then(() => {
+        return changeTsbDeviceStatus(row.deviceId, row.status)
+      }).then(() => {
+        this.$modal.msgSuccess(text + '成功')
+      }).catch(() => {
+        row.status = row.status === '0' ? '1' : '0'
+      })
+    },
+    resetDeviceForm() {
+      this.deviceForm = {
+        deviceId: undefined,
+        imei: undefined,
+        deviceType: undefined,
+        deviceSn: undefined,
+        deviceProduceDate: undefined,
+        softwareVersion: undefined,
+        status: '0',
+        remark: undefined
+      }
+      this.resetForm('deviceForm')
+    },
+    openDeviceBindUser(row) {
+      this.bindScopeDevice = row
+      this.bindSelectedUserId = undefined
+      this.currentDeviceBind = null
+      this.unboundUserOptions = []
+      this.deviceBindOpen = true
+    },
+    onDeviceBindDialogOpen() {
+      this.refreshDeviceBindDialog()
+    },
+    refreshDeviceBindDialog() {
+      if (!this.bindScopeDevice) {
+        return
+      }
+      const deviceId = this.bindScopeDevice.deviceId
+      Promise.all([
+        getTsbBindByDevice(deviceId),
+        listTsbUnboundUsers()
+      ]).then(([bindRes, usersRes]) => {
+        this.currentDeviceBind = bindRes.data || null
+        this.unboundUserOptions = usersRes.data || []
+      })
+    },
+    submitDeviceBind() {
+      if (!this.bindScopeDevice) {
+        return
+      }
+      if (this.currentDeviceBind) {
+        this.$modal.msgWarning('请先解绑当前用户后再绑定')
+        return
+      }
+      if (this.bindSelectedUserId == null) {
+        this.$modal.msgWarning('请选择用户')
+        return
+      }
+      bindTsbUserDevice(
+        this.bindSelectedUserId,
+        this.bindScopeDevice.deviceId
+      ).then(() => {
+        this.$modal.msgSuccess('绑定成功')
+        this.bindSelectedUserId = undefined
+        this.refreshDeviceBindDialog()
+      })
+    },
+    handleUnbindCurrentUser() {
+      if (!this.currentDeviceBind) {
+        return
+      }
+      this.$modal.confirm('是否确认解绑该用户与当前设备?').then(() => {
+        return unbindTsbUserDevice(
+          this.currentDeviceBind.userId,
+          this.bindScopeDevice.deviceId
+        )
+      }).then(() => {
+        this.$modal.msgSuccess('解绑成功')
+        this.refreshDeviceBindDialog()
+      }).catch(() => {})
+    }
+  }
+}
+</script>

+ 66 - 0
sql/tsb_3.0.sql

@@ -0,0 +1,66 @@
+-- ----------------------------
+-- 调试宝设备表(TSB Device)
+-- 执行前请确认库名;可与 ry_20260417.sql 使用同一数据库
+-- ----------------------------
+DROP TABLE IF EXISTS tsb_user_device;
+DROP TABLE IF EXISTS tsb_device;
+CREATE TABLE tsb_device (
+  device_id            bigint(20)      NOT NULL AUTO_INCREMENT    COMMENT '设备主键',
+  imei                 varchar(32)     NOT NULL                   COMMENT '设备IMEI(唯一)',
+  device_type          varchar(32)     NULL                       COMMENT '设备型号',
+  device_sn            bigint          NULL                       COMMENT '设备SN码',
+  device_produce_date  datetime        NULL                       COMMENT '设备生产日期',
+  software_version     varchar(32)     NULL                       COMMENT '软件版本号',
+  status               char(1)         NULL                       COMMENT '状态(0正常 1停用)',
+  last_run_time        datetime        NULL                       COMMENT '最后在线时间',
+  del_flag             char(1)         DEFAULT '0'                COMMENT '删除标志(0代表存在 2代表删除)',
+  create_by            varchar(64)     DEFAULT ''                 COMMENT '创建者',
+  create_time          datetime                                   COMMENT '创建时间',
+  update_by            varchar(64)     DEFAULT ''                 COMMENT '更新者',
+  update_time          datetime                                   COMMENT '更新时间',
+  remark               varchar(500)    DEFAULT NULL               COMMENT '备注',
+  PRIMARY KEY (device_id),
+  UNIQUE KEY uk_imei (imei),
+  KEY idx_device_sn (device_sn),
+  KEY idx_status_del (status, del_flag)
+) COMMENT='调试宝设备表';
+
+
+-- ----------------------------
+-- 调试宝设备与用户关联表(一用户一台在用设备)
+-- user_id、device_id 各自唯一,与 sys_user.user_id、tsb_device.device_id 对应
+-- ----------------------------
+CREATE TABLE tsb_user_device (
+  user_id            bigint(20)      NOT NULL                   COMMENT '用户ID(sys_user.user_id)',
+  device_id          bigint(20)      NOT NULL                   COMMENT '设备ID(tsb_device.device_id)',
+  bind_time          datetime        NULL                       COMMENT '绑定时间',
+  PRIMARY KEY (user_id, device_id)
+) COMMENT='调试宝设备与用户绑定表';
+
+
+-- ----------------------------
+-- 调试宝 菜单与按钮(若依 sys_menu)
+-- 在 ry_*.sql 初始化库执行后追加;执行完需在「角色管理」中为普通角色勾选新菜单,或为 sys_role_menu 追加见文末
+-- ----------------------------
+
+-- 一级目录
+INSERT INTO sys_menu VALUES ('3200', '调试宝', '0', '5', 'tsb', NULL, '', '', 1, 0, 'M', '0', '0', '', 'phone', 'admin', sysdate(), '', NULL, '调试宝目录');
+
+-- 设备管理页(含列表中的绑定能力)
+INSERT INTO sys_menu VALUES ('3201', '设备管理', '3200', '1', 'device', 'tsb/device/index', '', '', 1, 0, 'C', '0', '0', 'tsb:device:list', 'list', 'admin', sysdate(), '', NULL, '调试宝设备管理');
+
+-- 按钮
+INSERT INTO sys_menu VALUES ('3202', '设备查询', '3201', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'tsb:device:query', '#', 'admin', sysdate(), '', NULL, '');
+INSERT INTO sys_menu VALUES ('3203', '设备新增', '3201', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'tsb:device:add', '#', 'admin', sysdate(), '', NULL, '');
+INSERT INTO sys_menu VALUES ('3204', '设备修改', '3201', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'tsb:device:edit', '#', 'admin', sysdate(), '', NULL, '');
+INSERT INTO sys_menu VALUES ('3205', '设备删除', '3201', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'tsb:device:remove', '#', 'admin', sysdate(), '', NULL, '');
+INSERT INTO sys_menu VALUES ('3206', '设备导出', '3201', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'tsb:device:export', '#', 'admin', sysdate(), '', NULL, '');
+
+-- 普通角色 role_id=2 授权(超级管理员通常拥有 *:*:*,可不执行;按需对其它角色同样插入)
+INSERT INTO sys_role_menu VALUES ('2', '3200');
+INSERT INTO sys_role_menu VALUES ('2', '3201');
+INSERT INTO sys_role_menu VALUES ('2', '3202');
+INSERT INTO sys_role_menu VALUES ('2', '3203');
+INSERT INTO sys_role_menu VALUES ('2', '3204');
+INSERT INTO sys_role_menu VALUES ('2', '3205');
+INSERT INTO sys_role_menu VALUES ('2', '3206');