云联一号 2 minggu lalu
induk
melakukan
1543eb2546
28 mengubah file dengan 656 tambahan dan 20 penghapusan
  1. 15 0
      fs-admin/src/main/java/com/fs/admin/controller/AdminProxyController.java
  2. 23 0
      fs-admin/src/main/java/com/fs/admin/controller/CompanyAdminController.java
  3. 56 1
      fs-admin/src/main/java/com/fs/web/controller/system/CompanySmsPortController.java
  4. 7 7
      fs-company/src/main/java/com/fs/company/SmsApiMigration.java
  5. 2 2
      fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java
  6. 3 3
      fs-service/src/main/java/com/fs/proxy/domain/CompanySmsApi.java
  7. 6 0
      fs-service/src/main/java/com/fs/proxy/domain/CompanySmsCard.java
  8. 3 0
      fs-service/src/main/java/com/fs/proxy/domain/CompanySmsCardMiddleware.java
  9. 61 0
      fs-service/src/main/java/com/fs/proxy/domain/CompanySmsDevice.java
  10. 3 0
      fs-service/src/main/java/com/fs/proxy/domain/Proxy.java
  11. 35 0
      fs-service/src/main/java/com/fs/proxy/mapper/CompanySmsDeviceMapper.java
  12. 2 0
      fs-service/src/main/java/com/fs/proxy/mapper/ProxyMapper.java
  13. 17 2
      fs-service/src/main/java/com/fs/proxy/service/ICompanySmsPortService.java
  14. 2 0
      fs-service/src/main/java/com/fs/proxy/service/ProxyService.java
  15. 80 0
      fs-service/src/main/java/com/fs/proxy/service/impl/CompanySmsPortServiceImpl.java
  16. 10 0
      fs-service/src/main/java/com/fs/proxy/service/impl/ProxyServiceImpl.java
  17. 1 1
      fs-service/src/main/java/com/fs/sms/service/impl/SmsTServiceImpl.java
  18. 7 0
      fs-service/src/main/java/com/fs/tenant/mapper/TenantInfoMapper.java
  19. 8 0
      fs-service/src/main/java/com/fs/tenant/service/TenantInfoService.java
  20. 6 0
      fs-service/src/main/java/com/fs/tenant/service/impl/TenantInfoServiceImpl.java
  21. 9 3
      fs-service/src/main/resources/mapper/proxy/CompanySmsCardMapper.xml
  22. 107 0
      fs-service/src/main/resources/mapper/proxy/CompanySmsDeviceMapper.xml
  23. 8 1
      fs-service/src/main/resources/mapper/proxy/ProxyMapper.xml
  24. 5 0
      fs-service/src/main/resources/mapper/tenant/TenantInfoMapper.xml
  25. 11 0
      sql/add_sms_card_menu.sql
  26. 93 0
      sql/add_sms_device.sql
  27. 26 0
      sql/add_sms_device_menu.sql
  28. 50 0
      sql/add_voice_pricing.sql

+ 15 - 0
fs-admin/src/main/java/com/fs/admin/controller/AdminProxyController.java

@@ -1,6 +1,7 @@
 package com.fs.admin.controller;
 package com.fs.admin.controller;
 
 
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 
 
 import com.fs.common.annotation.ProxyLog;
 import com.fs.common.annotation.ProxyLog;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.controller.BaseController;
@@ -100,4 +101,18 @@ public class AdminProxyController extends BaseController
         List<Proxy> list = proxyService.selectProxyList(query);
         List<Proxy> list = proxyService.selectProxyList(query);
         return AjaxResult.success(list);
         return AjaxResult.success(list);
     }
     }
+
+    /**
+     * 重置代理密码
+     */
+    @PreAuthorize("@ss.hasPermi('admin:proxy:edit')")
+    @ProxyLog(title = "代理管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/resetPwd/{proxyId}")
+    public AjaxResult resetPwd(@PathVariable Long proxyId, @RequestBody Map<String, String> params) {
+        String password = params.get("password");
+        if (password == null || password.length() < 6) {
+            return AjaxResult.error("密码长度不能少于6位");
+        }
+        return toAjax(proxyService.resetPwd(proxyId, password));
+    }
 }
 }

+ 23 - 0
fs-admin/src/main/java/com/fs/admin/controller/CompanyAdminController.java

@@ -259,4 +259,27 @@ public class CompanyAdminController extends BaseController {
         }
         }
         return new ArrayList<>();
         return new ArrayList<>();
     }
     }
+
+    /**
+     * 重置租户管理员密码
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:edit')")
+    @Log(title = "租户管理", businessType = BusinessType.UPDATE)
+    @PutMapping("/{id}/resetPwd")
+    public AjaxResult resetPwd(@PathVariable String id, @RequestBody Map<String, String> params) {
+        String password = params.get("password");
+        if (password == null || password.length() < 6) {
+            return AjaxResult.error("密码长度不能少于6位");
+        }
+        TenantInfo tenantInfo = tenantInfoMapper.selectTenantInfoById(id);
+        if (tenantInfo == null) {
+            return AjaxResult.error("租户不存在");
+        }
+        try {
+            tenantDataSourceManager.switchTenant(tenantInfo);
+            return toAjax(tenantInfoService.resetTenantAdminPwd(Long.valueOf(id), password));
+        } finally {
+            tenantDataSourceManager.clear();
+        }
+    }
 }
 }

+ 56 - 1
fs-admin/src/main/java/com/fs/web/controller/system/CompanySmsPortController.java

@@ -129,7 +129,7 @@ public class CompanySmsPortController extends BaseController {
 
 
     // ========== 手机卡心跳(无需登录鉴权,手机APP调用) ==========
     // ========== 手机卡心跳(无需登录鉴权,手机APP调用) ==========
 
 
-    /** 心跳上报 */
+    /** 心跳上报(旧接口,兼容保留) */
     @PostMapping("/card/heartbeat")
     @PostMapping("/card/heartbeat")
     public AjaxResult heartbeat(@RequestParam String imei,
     public AjaxResult heartbeat(@RequestParam String imei,
                                 @RequestParam(required = false) String appVersion,
                                 @RequestParam(required = false) String appVersion,
@@ -141,6 +141,61 @@ public class CompanySmsPortController extends BaseController {
         return AjaxResult.success();
         return AjaxResult.success();
     }
     }
 
 
+    // ========== 设备管理 ==========
+
+    /** 查询设备列表 */
+    @PreAuthorize("@ss.hasPermi('platform:smsDevice:list')")
+    @GetMapping("/device/list")
+    public AjaxResult deviceList(CompanySmsDevice query) {
+        List<CompanySmsDevice> list = portService.selectDeviceList(query);
+        return AjaxResult.success(list);
+    }
+
+    /** 设备详情 */
+    @PreAuthorize("@ss.hasPermi('platform:smsDevice:query')")
+    @GetMapping("/device/{deviceId}")
+    public AjaxResult deviceInfo(@PathVariable Long deviceId) {
+        return AjaxResult.success(portService.selectDeviceById(deviceId));
+    }
+
+    /** 新增设备 */
+    @PreAuthorize("@ss.hasPermi('platform:smsDevice:add')")
+    @PostMapping("/device")
+    public AjaxResult addDevice(@RequestBody CompanySmsDevice device) {
+        device.setCreateBy(getUsername());
+        return toAjax(portService.insertDevice(device));
+    }
+
+    /** 修改设备 */
+    @PreAuthorize("@ss.hasPermi('platform:smsDevice:edit')")
+    @PutMapping("/device")
+    public AjaxResult editDevice(@RequestBody CompanySmsDevice device) {
+        device.setUpdateBy(getUsername());
+        return toAjax(portService.updateDevice(device));
+    }
+
+    /** 删除设备 */
+    @PreAuthorize("@ss.hasPermi('platform:smsDevice:remove')")
+    @DeleteMapping("/device/{deviceId}")
+    public AjaxResult removeDevice(@PathVariable Long deviceId) {
+        return toAjax(portService.deleteDeviceById(deviceId));
+    }
+
+    /** 分配设备给销售 */
+    @PreAuthorize("@ss.hasPermi('platform:smsDevice:edit')")
+    @PutMapping("/device/assign")
+    public AjaxResult assignDevice(@RequestParam Long deviceId, @RequestParam Long companyUserId) {
+        return toAjax(portService.assignDeviceUser(deviceId, companyUserId));
+    }
+
+    /** 设备心跳上报(无需登录鉴权,手机APP调用) */
+    @PostMapping("/device/heartbeat")
+    public AjaxResult deviceHeartbeat(@RequestParam String imei,
+                                      @RequestParam(required = false) String appVersion) {
+        portService.deviceHeartbeat(imei, appVersion);
+        return AjaxResult.success();
+    }
+
     // ========== 中间件管理 ==========
     // ========== 中间件管理 ==========
 
 
     /** 查询中间件列表 */
     /** 查询中间件列表 */

+ 7 - 7
fs-company/src/main/java/com/fs/company/SmsApiMigration.java

@@ -19,12 +19,12 @@ public class SmsApiMigration {
         String createApiTable = "CREATE TABLE IF NOT EXISTS `company_sms_api` (" +
         String createApiTable = "CREATE TABLE IF NOT EXISTS `company_sms_api` (" +
             "`api_id` bigint NOT NULL AUTO_INCREMENT COMMENT '接口ID'," +
             "`api_id` bigint NOT NULL AUTO_INCREMENT COMMENT '接口ID'," +
             "`api_name` varchar(100) NOT NULL COMMENT '接口名称'," +
             "`api_name` varchar(100) NOT NULL COMMENT '接口名称'," +
-            "`provider` varchar(20) NOT NULL COMMENT '服务商: rf润方/dh德华'," +
+            "`provider` varchar(20) NOT NULL COMMENT '服务商: rf迈远/dh德华'," +
             "`temp_type` int NOT NULL COMMENT '场景类型: 1营销 2通知'," +
             "`temp_type` int NOT NULL COMMENT '场景类型: 1营销 2通知'," +
             "`account` varchar(100) DEFAULT NULL COMMENT '账户名'," +
             "`account` varchar(100) DEFAULT NULL COMMENT '账户名'," +
             "`password` varchar(100) DEFAULT NULL COMMENT '密码'," +
             "`password` varchar(100) DEFAULT NULL COMMENT '密码'," +
-            "`url` varchar(255) DEFAULT NULL COMMENT '接口地址(润方专用)'," +
-            "`code` varchar(50) DEFAULT NULL COMMENT '扩展码(润方专用)'," +
+            "`url` varchar(255) DEFAULT NULL COMMENT '接口地址(迈远专用)'," +
+            "`code` varchar(50) DEFAULT NULL COMMENT '扩展码(迈远专用)'," +
             "`sign` varchar(50) DEFAULT NULL COMMENT '短信签名'," +
             "`sign` varchar(50) DEFAULT NULL COMMENT '短信签名'," +
             "`status` int DEFAULT 1 COMMENT '状态 0禁用 1正常'," +
             "`status` int DEFAULT 1 COMMENT '状态 0禁用 1正常'," +
             "`remark` varchar(500) DEFAULT NULL COMMENT '备注'," +
             "`remark` varchar(500) DEFAULT NULL COMMENT '备注'," +
@@ -91,7 +91,7 @@ public class SmsApiMigration {
 
 
                 // Insert rf marketing channel if account exists
                 // Insert rf marketing channel if account exists
                 if (rfAccount1 != null && !rfAccount1.isEmpty()) {
                 if (rfAccount1 != null && !rfAccount1.isEmpty()) {
-                    ps.setString(1, "润方营销通道");
+                    ps.setString(1, "迈远营销通道");
                     ps.setString(2, "rf");
                     ps.setString(2, "rf");
                     ps.setInt(3, 1);
                     ps.setInt(3, 1);
                     ps.setString(4, rfAccount1);
                     ps.setString(4, rfAccount1);
@@ -100,12 +100,12 @@ public class SmsApiMigration {
                     ps.setString(7, rfCode1);
                     ps.setString(7, rfCode1);
                     ps.setString(8, rfSign);
                     ps.setString(8, rfSign);
                     ps.executeUpdate();
                     ps.executeUpdate();
-                    pw.println("  - Inserted: 润方营销通道");
+                    pw.println("  - Inserted: 迈远营销通道");
                 }
                 }
 
 
                 // Insert rf notification channel
                 // Insert rf notification channel
                 if (rfAccount2 != null && !rfAccount2.isEmpty()) {
                 if (rfAccount2 != null && !rfAccount2.isEmpty()) {
-                    ps.setString(1, "润方通知通道");
+                    ps.setString(1, "迈远通知通道");
                     ps.setString(2, "rf");
                     ps.setString(2, "rf");
                     ps.setInt(3, 2);
                     ps.setInt(3, 2);
                     ps.setString(4, rfAccount2);
                     ps.setString(4, rfAccount2);
@@ -114,7 +114,7 @@ public class SmsApiMigration {
                     ps.setString(7, rfCode2);
                     ps.setString(7, rfCode2);
                     ps.setString(8, rfSign);
                     ps.setString(8, rfSign);
                     ps.executeUpdate();
                     ps.executeUpdate();
-                    pw.println("  - Inserted: 润方通知通道");
+                    pw.println("  - Inserted: 迈远通知通道");
                 }
                 }
 
 
                 // Insert dh marketing channel
                 // Insert dh marketing channel

+ 2 - 2
fs-service/src/main/java/com/fs/common/service/impl/SmsServiceImpl.java

@@ -154,7 +154,7 @@ public class SmsServiceImpl implements ISmsService
         log.info("resolveAndSend: provider={}, apiId={}, portId={}, phone={}", provider, api.getApiId(), port.getPortId(), phone);
         log.info("resolveAndSend: provider={}, apiId={}, portId={}, phone={}", provider, api.getApiId(), port.getPortId(), phone);
 
 
         if ("rf".equals(provider)) {
         if ("rf".equals(provider)) {
-            // 润方发送
+            // 迈远发送
             return sendByRf(phone, content, tempType, useAccount, usePassword, useSign, useUrl, port.getPortNo(), tenantId, api.getApiId(), port.getPortId());
             return sendByRf(phone, content, tempType, useAccount, usePassword, useSign, useUrl, port.getPortNo(), tenantId, api.getApiId(), port.getPortId());
         } else if ("dh".equals(provider)) {
         } else if ("dh".equals(provider)) {
             // 德华发送
             // 德华发送
@@ -168,7 +168,7 @@ public class SmsServiceImpl implements ISmsService
         }
         }
     }
     }
 
 
-    /** 润方发送 */
+    /** 迈远发送 */
     private String sendByRf(String phone, String content, Integer tempType,
     private String sendByRf(String phone, String content, Integer tempType,
                              String account, String password, String sign, String url, String extno,
                              String account, String password, String sign, String url, String extno,
                              Long tenantId, Long apiId, Long portId) {
                              Long tenantId, Long apiId, Long portId) {

+ 3 - 3
fs-service/src/main/java/com/fs/proxy/domain/CompanySmsApi.java

@@ -20,7 +20,7 @@ public class CompanySmsApi extends BaseEntity {
     @Excel(name = "接口名称")
     @Excel(name = "接口名称")
     private String apiName;
     private String apiName;
 
 
-    /** 服务商: rf润方/dh德华/card手机卡 */
+    /** 服务商: rf迈远/dh德华/card手机卡 */
     @Excel(name = "服务商")
     @Excel(name = "服务商")
     private String provider;
     private String provider;
 
 
@@ -35,10 +35,10 @@ public class CompanySmsApi extends BaseEntity {
     /** 密码 */
     /** 密码 */
     private String password;
     private String password;
 
 
-    /** 接口地址(润方专用) */
+    /** 接口地址(迈远专用) */
     private String url;
     private String url;
 
 
-    /** 扩展码(润方专用) */
+    /** 扩展码(迈远专用) */
     private String code;
     private String code;
 
 
     /** 短信签名 */
     /** 短信签名 */

+ 6 - 0
fs-service/src/main/java/com/fs/proxy/domain/CompanySmsCard.java

@@ -22,6 +22,12 @@ public class CompanySmsCard extends BaseEntity {
     /** 所属租户 */
     /** 所属租户 */
     private Long tenantId;
     private Long tenantId;
 
 
+    /** 所属设备ID */
+    private Long deviceId;
+
+    /** 卡槽位置(1或2) */
+    private Integer slotIndex;
+
     /** 手机IMEI */
     /** 手机IMEI */
     private String imei;
     private String imei;
 
 

+ 3 - 0
fs-service/src/main/java/com/fs/proxy/domain/CompanySmsCardMiddleware.java

@@ -16,6 +16,9 @@ public class CompanySmsCardMiddleware extends BaseEntity {
     /** 接口ID(provider=card) */
     /** 接口ID(provider=card) */
     private Long apiId;
     private Long apiId;
 
 
+    /** 所属租户 */
+    private Long tenantId;
+
     /** 中间件名称 */
     /** 中间件名称 */
     private String middlewareName;
     private String middlewareName;
 
 

+ 61 - 0
fs-service/src/main/java/com/fs/proxy/domain/CompanySmsDevice.java

@@ -0,0 +1,61 @@
+package com.fs.proxy.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 手机设备管理对象 company_sms_device
+ *
+ * @author fs
+ * @date 2026-05-24
+ */
+@Data
+public class CompanySmsDevice extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** 设备ID */
+    private Long deviceId;
+
+    /** 所属租户 */
+    private Long tenantId;
+
+    /** 绑定销售用户ID(NULL=未分配) */
+    private Long companyUserId;
+
+    /** 设备名称 */
+    private String deviceName;
+
+    /** IMEI(唯一标识) */
+    private String imei;
+
+    /** APP版本 */
+    private String appVersion;
+
+    /** 关联中间件ID */
+    private Long middlewareId;
+
+    /** 最后心跳时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date lastHeartbeat;
+
+    /** 状态: 0离线/1在线/2禁用 */
+    private Integer status;
+
+    /** 备注 */
+    private String remark;
+
+    // ========== 关联查询字段(非表字段) ==========
+    /** 租户名称 */
+    private String tenantName;
+    /** 销售用户名称 */
+    private String userName;
+    /** 中间件名称 */
+    private String middlewareName;
+    /** 卡数量 */
+    private Integer cardCount;
+    /** 在线卡数量 */
+    private Integer onlineCardCount;
+}

+ 3 - 0
fs-service/src/main/java/com/fs/proxy/domain/Proxy.java

@@ -74,6 +74,9 @@ public class Proxy extends BaseEntity
     /** 备注 */
     /** 备注 */
     private String remark;
     private String remark;
 
 
+    /** 密码(登录用) */
+    private String password;
+
     /** 是否删除 */
     /** 是否删除 */
     private Integer isDel;
     private Integer isDel;
 
 

+ 35 - 0
fs-service/src/main/java/com/fs/proxy/mapper/CompanySmsDeviceMapper.java

@@ -0,0 +1,35 @@
+package com.fs.proxy.mapper;
+
+import com.fs.proxy.domain.CompanySmsDevice;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 设备管理 Mapper
+ */
+@Mapper
+public interface CompanySmsDeviceMapper {
+
+    List<CompanySmsDevice> selectDeviceList(CompanySmsDevice query);
+
+    CompanySmsDevice selectDeviceById(@Param("deviceId") Long deviceId);
+
+    CompanySmsDevice selectDeviceByImei(@Param("imei") String imei);
+
+    int insertDevice(CompanySmsDevice device);
+
+    int updateDevice(CompanySmsDevice device);
+
+    /** 分配销售: 设置 device.company_user_id */
+    int assignUser(@Param("deviceId") Long deviceId, @Param("companyUserId") Long companyUserId);
+
+    int deleteDeviceById(@Param("deviceId") Long deviceId);
+
+    /** 心跳更新: 按imei更新心跳时间+在线状态 */
+    int updateHeartbeat(@Param("imei") String imei, @Param("appVersion") String appVersion);
+
+    /** 批量更新离线状态(超过指定秒数无心跳的设为离线) */
+    int updateOfflineDevices(@Param("timeoutSeconds") int timeoutSeconds);
+}

+ 2 - 0
fs-service/src/main/java/com/fs/proxy/mapper/ProxyMapper.java

@@ -28,4 +28,6 @@ public interface ProxyMapper
     int deleteProxyByIds(Long[] proxyIds);
     int deleteProxyByIds(Long[] proxyIds);
 
 
     int checkNameUnique(@Param("proxyName") String proxyName, @Param("proxyId") Long proxyId);
     int checkNameUnique(@Param("proxyName") String proxyName, @Param("proxyId") Long proxyId);
+
+    int resetPwd(@Param("proxyId") Long proxyId, @Param("password") String password);
 }
 }

+ 17 - 2
fs-service/src/main/java/com/fs/proxy/service/ICompanySmsPortService.java

@@ -4,11 +4,12 @@ import com.fs.proxy.domain.CompanySmsApiPort;
 import com.fs.proxy.domain.CompanySmsPortAssign;
 import com.fs.proxy.domain.CompanySmsPortAssign;
 import com.fs.proxy.domain.CompanySmsCard;
 import com.fs.proxy.domain.CompanySmsCard;
 import com.fs.proxy.domain.CompanySmsCardMiddleware;
 import com.fs.proxy.domain.CompanySmsCardMiddleware;
+import com.fs.proxy.domain.CompanySmsDevice;
 
 
 import java.util.List;
 import java.util.List;
 
 
 /**
 /**
- * 短信端口+卡管理+中间件 Service
+ * 短信端口+卡管理+中间件+设备管理 Service
  */
  */
 public interface ICompanySmsPortService {
 public interface ICompanySmsPortService {
 
 
@@ -29,6 +30,20 @@ public interface ICompanySmsPortService {
     int updateAssign(CompanySmsPortAssign assign);
     int updateAssign(CompanySmsPortAssign assign);
     int deleteAssignById(Long id);
     int deleteAssignById(Long id);
 
 
+    // ========== 设备管理 ==========
+    List<CompanySmsDevice> selectDeviceList(CompanySmsDevice query);
+    CompanySmsDevice selectDeviceById(Long deviceId);
+    CompanySmsDevice selectDeviceByImei(String imei);
+    int insertDevice(CompanySmsDevice device);
+    int updateDevice(CompanySmsDevice device);
+    int deleteDeviceById(Long deviceId);
+    /** 分配设备给销售 */
+    int assignDeviceUser(Long deviceId, Long companyUserId);
+    /** 心跳上报(设备级) */
+    int deviceHeartbeat(String imei, String appVersion);
+    /** 批量更新离线设备(含其下卡) */
+    int updateOfflineDevices();
+
     // ========== 手机卡管理 ==========
     // ========== 手机卡管理 ==========
     List<CompanySmsCard> selectCardList(CompanySmsCard query);
     List<CompanySmsCard> selectCardList(CompanySmsCard query);
     CompanySmsCard selectCardById(Long cardId);
     CompanySmsCard selectCardById(Long cardId);
@@ -36,7 +51,7 @@ public interface ICompanySmsPortService {
     int insertCard(CompanySmsCard card);
     int insertCard(CompanySmsCard card);
     int updateCard(CompanySmsCard card);
     int updateCard(CompanySmsCard card);
     int deleteCardById(Long cardId);
     int deleteCardById(Long cardId);
-    /** 心跳上报 */
+    /** 心跳上报(兼容旧接口, 委托到deviceHeartbeat) */
     int heartbeat(String imei, String appVersion, String phone1, String phone2, String deviceName, Long tenantId);
     int heartbeat(String imei, String appVersion, String phone1, String phone2, String deviceName, Long tenantId);
     /** 批量更新离线卡 */
     /** 批量更新离线卡 */
     int updateOfflineCards();
     int updateOfflineCards();

+ 2 - 0
fs-service/src/main/java/com/fs/proxy/service/ProxyService.java

@@ -27,4 +27,6 @@ public interface ProxyService
     int deleteProxyByIds(Long[] proxyIds);
     int deleteProxyByIds(Long[] proxyIds);
 
 
     boolean checkNameUnique(Proxy proxy);
     boolean checkNameUnique(Proxy proxy);
+
+    int resetPwd(Long proxyId, String password);
 }
 }

+ 80 - 0
fs-service/src/main/java/com/fs/proxy/service/impl/CompanySmsPortServiceImpl.java

@@ -25,6 +25,8 @@ public class CompanySmsPortServiceImpl implements ICompanySmsPortService {
     private CompanySmsCardMiddlewareMapper middlewareMapper;
     private CompanySmsCardMiddlewareMapper middlewareMapper;
     @Autowired
     @Autowired
     private CompanySmsApiTenantMapper tenantMapper;
     private CompanySmsApiTenantMapper tenantMapper;
+    @Autowired
+    private CompanySmsDeviceMapper deviceMapper;
 
 
     // ========== 端口池 ==========
     // ========== 端口池 ==========
 
 
@@ -191,6 +193,84 @@ public class CompanySmsPortServiceImpl implements ICompanySmsPortService {
         return assignMapper.deleteAssignById(id);
         return assignMapper.deleteAssignById(id);
     }
     }
 
 
+    // ========== 设备管理 ==========
+
+    @Override
+    public List<CompanySmsDevice> selectDeviceList(CompanySmsDevice query) {
+        return deviceMapper.selectDeviceList(query);
+    }
+
+    @Override
+    public CompanySmsDevice selectDeviceById(Long deviceId) {
+        return deviceMapper.selectDeviceById(deviceId);
+    }
+
+    @Override
+    public CompanySmsDevice selectDeviceByImei(String imei) {
+        return deviceMapper.selectDeviceByImei(imei);
+    }
+
+    @Override
+    public int insertDevice(CompanySmsDevice device) {
+        return deviceMapper.insertDevice(device);
+    }
+
+    @Override
+    public int updateDevice(CompanySmsDevice device) {
+        return deviceMapper.updateDevice(device);
+    }
+
+    @Override
+    public int deleteDeviceById(Long deviceId) {
+        return deviceMapper.deleteDeviceById(deviceId);
+    }
+
+    @Override
+    public int assignDeviceUser(Long deviceId, Long companyUserId) {
+        return deviceMapper.assignUser(deviceId, companyUserId);
+    }
+
+    /**
+     * 设备心跳上报(新的核心方法):
+     * 1. 按imei查设备, 存在则更新心跳+状态为在线
+     * 2. 设备不存在则自动注册新设备
+     * 3. 设备下的卡同步更新状态为在线
+     */
+    @Override
+    public int deviceHeartbeat(String imei, String appVersion) {
+        CompanySmsDevice existing = deviceMapper.selectDeviceByImei(imei);
+        if (existing != null) {
+            // 设备存在: 更新心跳
+            int rows = deviceMapper.updateHeartbeat(imei, appVersion);
+            // 同步更新该设备下卡的状态为在线
+            cardMapper.updateHeartbeat(imei, appVersion);
+            return rows;
+        }
+        // 设备不存在: 自动注册(但此时不知道租户, 先设为null, adminUI后续可修改)
+        CompanySmsDevice device = new CompanySmsDevice();
+        device.setImei(imei);
+        device.setAppVersion(appVersion);
+        device.setDeviceName("未知设备");
+        device.setStatus(1); // 在线
+        device.setLastHeartbeat(new java.util.Date());
+        log.info("deviceHeartbeat: 自动注册新设备 imei={}", imei);
+        return deviceMapper.insertDevice(device);
+    }
+
+    /**
+     * 批量更新离线设备, 同时同步其下的卡为离线
+     */
+    @Override
+    public int updateOfflineDevices() {
+        // 先标记离线设备
+        int rows = deviceMapper.updateOfflineDevices(90);
+        // 同步: 离线设备下的卡也标记为离线
+        if (rows > 0) {
+            cardMapper.updateOfflineCards(90);
+        }
+        return rows;
+    }
+
     // ========== 手机卡管理 ==========
     // ========== 手机卡管理 ==========
 
 
     @Override
     @Override

+ 10 - 0
fs-service/src/main/java/com/fs/proxy/service/impl/ProxyServiceImpl.java

@@ -3,6 +3,7 @@ package com.fs.proxy.service.impl;
 import java.util.List;
 import java.util.List;
 
 
 import com.fs.common.exception.ServiceException;
 import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.SecurityUtils;
 import com.fs.proxy.domain.Proxy;
 import com.fs.proxy.domain.Proxy;
 import com.fs.proxy.mapper.ProxyMapper;
 import com.fs.proxy.mapper.ProxyMapper;
 import com.fs.proxy.service.ProxyService;
 import com.fs.proxy.service.ProxyService;
@@ -43,6 +44,9 @@ public class ProxyServiceImpl implements ProxyService
         if (!checkNameUnique(proxy)) {
         if (!checkNameUnique(proxy)) {
             throw new ServiceException("代理名称已存在");
             throw new ServiceException("代理名称已存在");
         }
         }
+        if (proxy.getPassword() != null && !proxy.getPassword().isEmpty()) {
+            proxy.setPassword(SecurityUtils.encryptPassword(proxy.getPassword()));
+        }
         return proxyMapper.insertProxy(proxy);
         return proxyMapper.insertProxy(proxy);
     }
     }
 
 
@@ -73,4 +77,10 @@ public class ProxyServiceImpl implements ProxyService
         int count = proxyMapper.checkNameUnique(proxy.getProxyName(), proxyId);
         int count = proxyMapper.checkNameUnique(proxy.getProxyName(), proxyId);
         return count == 0;
         return count == 0;
     }
     }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int resetPwd(Long proxyId, String password) {
+        return proxyMapper.resetPwd(proxyId, SecurityUtils.encryptPassword(password));
+    }
 }
 }

+ 1 - 1
fs-service/src/main/java/com/fs/sms/service/impl/SmsTServiceImpl.java

@@ -41,7 +41,7 @@ public class SmsTServiceImpl implements SmsService {
             ClientProfile clientProfile = new ClientProfile();
             ClientProfile clientProfile = new ClientProfile();
             clientProfile.setHttpProfile(httpProfile);
             clientProfile.setHttpProfile(httpProfile);
             SmsClient client = new SmsClient(cred, "ap-beijing", clientProfile);
             SmsClient client = new SmsClient(cred, "ap-beijing", clientProfile);
-            String params = "{\"PhoneNumberSet\":[\"86" + mobile + "\"],\"TemplateID\":\"1982054\",\"Sign\":\"重庆润方数字科技有限公司\",\"TemplateParamSet\":[\"" + code + "\"],\"SmsSdkAppid\":\"1400867365\"}";
+            String params = "{\"PhoneNumberSet\":[\"86" + mobile + "\"],\"TemplateID\":\"1982054\",\"Sign\":\"重庆迈远科技有限公司\",\"TemplateParamSet\":[\"" + code + "\"],\"SmsSdkAppid\":\"1400867365\"}";
             SendSmsRequest req = SendSmsRequest.fromJsonString(params, SendSmsRequest.class);
             SendSmsRequest req = SendSmsRequest.fromJsonString(params, SendSmsRequest.class);
 
 
             SendSmsResponse resp = client.SendSms(req);
             SendSmsResponse resp = client.SendSms(req);

+ 7 - 0
fs-service/src/main/java/com/fs/tenant/mapper/TenantInfoMapper.java

@@ -151,6 +151,13 @@ public interface TenantInfoMapper extends BaseMapper<TenantInfo> {
      * @return 影响行数
      * @return 影响行数
      */
      */
     int updateBalance(@Param("id") Long id, @Param("amount") java.math.BigDecimal amount);
     int updateBalance(@Param("id") Long id, @Param("amount") java.math.BigDecimal amount);
+
+    /**
+     * 重置租户管理员密码(sys_user表,user_id=1)
+     * @param password 加密后的密码
+     * @return 影响行数
+     */
+    int resetTenantAdminPwd(@Param("password") String password);
 }
 }
 
 
 
 

+ 8 - 0
fs-service/src/main/java/com/fs/tenant/service/TenantInfoService.java

@@ -127,4 +127,12 @@ public interface TenantInfoService extends IService<TenantInfo> {
      * @return 影响行数
      * @return 影响行数
      */
      */
     int updateBalance(Long id, BigDecimal amount);
     int updateBalance(Long id, BigDecimal amount);
+
+    /**
+     * 重置租户管理员密码
+     * @param id 租户ID
+     * @param password 新密码(明文,service层加密)
+     * @return 影响行数
+     */
+    int resetTenantAdminPwd(Long id, String password);
 }
 }

+ 6 - 0
fs-service/src/main/java/com/fs/tenant/service/impl/TenantInfoServiceImpl.java

@@ -19,6 +19,7 @@ import com.fs.common.core.domain.entity.TenantCompanyMenu;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.exception.CustomException;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.SecurityUtils;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.StringUtils;
 
 
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
 import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
@@ -679,6 +680,11 @@ public class TenantInfoServiceImpl extends ServiceImpl<TenantInfoMapper, TenantI
     public int updateBalance(Long id, BigDecimal amount) {
     public int updateBalance(Long id, BigDecimal amount) {
         return baseMapper.updateBalance(id, amount);
         return baseMapper.updateBalance(id, amount);
     }
     }
+
+    @Override
+    public int resetTenantAdminPwd(Long id, String password) {
+        return baseMapper.resetTenantAdminPwd(SecurityUtils.encryptPassword(password));
+    }
 }
 }
 
 
 
 

+ 9 - 3
fs-service/src/main/resources/mapper/proxy/CompanySmsCardMapper.xml

@@ -6,6 +6,8 @@
         <result property="cardId"        column="card_id"/>
         <result property="cardId"        column="card_id"/>
         <result property="portId"        column="port_id"/>
         <result property="portId"        column="port_id"/>
         <result property="tenantId"      column="tenant_id"/>
         <result property="tenantId"      column="tenant_id"/>
+        <result property="deviceId"      column="device_id"/>
+        <result property="slotIndex"     column="slot_index"/>
         <result property="imei"          column="imei"/>
         <result property="imei"          column="imei"/>
         <result property="deviceName"    column="device_name"/>
         <result property="deviceName"    column="device_name"/>
         <result property="simCount"      column="sim_count"/>
         <result property="simCount"      column="sim_count"/>
@@ -38,7 +40,7 @@
     </resultMap>
     </resultMap>
 
 
     <sql id="selectCardVo">
     <sql id="selectCardVo">
-        SELECT c.card_id, c.port_id, c.tenant_id, c.imei, c.device_name, c.sim_count,
+        SELECT c.card_id, c.port_id, c.tenant_id, c.device_id, c.slot_index, c.imei, c.device_name, c.sim_count,
                c.phone_1, c.phone_2, c.last_heartbeat, c.status, c.app_version,
                c.phone_1, c.phone_2, c.last_heartbeat, c.status, c.app_version,
                c.sms_sent_today, c.sms_sent_date, c.sms_sent_hour, c.sms_sent_hour_num,
                c.sms_sent_today, c.sms_sent_date, c.sms_sent_hour, c.sms_sent_hour_num,
                c.sms_hourly_limit, c.sms_daily_limit, c.sms_balance,
                c.sms_hourly_limit, c.sms_daily_limit, c.sms_balance,
@@ -51,12 +53,14 @@
         FROM company_sms_card c
         FROM company_sms_card c
         LEFT JOIN company_sms_api_port p ON c.port_id = p.port_id
         LEFT JOIN company_sms_api_port p ON c.port_id = p.port_id
         LEFT JOIN tenant_info ti ON c.tenant_id = ti.id
         LEFT JOIN tenant_info ti ON c.tenant_id = ti.id
+        LEFT JOIN company_sms_device d ON c.device_id = d.device_id
     </sql>
     </sql>
 
 
     <select id="selectCardList" resultMap="CardResult">
     <select id="selectCardList" resultMap="CardResult">
         <include refid="selectCardVo"/>
         <include refid="selectCardVo"/>
         <where>
         <where>
             <if test="tenantId != null">AND c.tenant_id = #{tenantId}</if>
             <if test="tenantId != null">AND c.tenant_id = #{tenantId}</if>
+            <if test="deviceId != null">AND c.device_id = #{deviceId}</if>
             <if test="imei != null and imei != ''">AND c.imei = #{imei}</if>
             <if test="imei != null and imei != ''">AND c.imei = #{imei}</if>
             <if test="status != null">AND c.status = #{status}</if>
             <if test="status != null">AND c.status = #{status}</if>
             <if test="phone1 != null and phone1 != ''">AND c.phone_1 = #{phone1}</if>
             <if test="phone1 != null and phone1 != ''">AND c.phone_1 = #{phone1}</if>
@@ -82,13 +86,13 @@
     </select>
     </select>
 
 
     <insert id="insertCard" useGeneratedKeys="true" keyProperty="cardId">
     <insert id="insertCard" useGeneratedKeys="true" keyProperty="cardId">
-        INSERT INTO company_sms_card (port_id, tenant_id, imei, device_name, sim_count,
+        INSERT INTO company_sms_card (port_id, tenant_id, device_id, slot_index, imei, device_name, sim_count,
             phone_1, phone_2, last_heartbeat, status, app_version,
             phone_1, phone_2, last_heartbeat, status, app_version,
             sms_hourly_limit, sms_daily_limit, sms_balance,
             sms_hourly_limit, sms_daily_limit, sms_balance,
             call_interval_seconds, call_minutes_balance, phone_bill_balance,
             call_interval_seconds, call_minutes_balance, phone_bill_balance,
             allow_call_forward, forward_phone,
             allow_call_forward, forward_phone,
             remark, create_time)
             remark, create_time)
-        VALUES (#{portId}, #{tenantId}, #{imei}, #{deviceName}, #{simCount},
+        VALUES (#{portId}, #{tenantId}, #{deviceId}, #{slotIndex}, #{imei}, #{deviceName}, #{simCount},
             #{phone1}, #{phone2}, #{lastHeartbeat}, #{status}, #{appVersion},
             #{phone1}, #{phone2}, #{lastHeartbeat}, #{status}, #{appVersion},
             #{smsHourlyLimit}, #{smsDailyLimit}, #{smsBalance},
             #{smsHourlyLimit}, #{smsDailyLimit}, #{smsBalance},
             #{callIntervalSeconds}, #{callMinutesBalance}, #{phoneBillBalance},
             #{callIntervalSeconds}, #{callMinutesBalance}, #{phoneBillBalance},
@@ -101,6 +105,8 @@
         <set>
         <set>
             <if test="portId != null">port_id = #{portId},</if>
             <if test="portId != null">port_id = #{portId},</if>
             <if test="tenantId != null">tenant_id = #{tenantId},</if>
             <if test="tenantId != null">tenant_id = #{tenantId},</if>
+            <if test="deviceId != null">device_id = #{deviceId},</if>
+            <if test="slotIndex != null">slot_index = #{slotIndex},</if>
             <if test="deviceName != null">device_name = #{deviceName},</if>
             <if test="deviceName != null">device_name = #{deviceName},</if>
             <if test="simCount != null">sim_count = #{simCount},</if>
             <if test="simCount != null">sim_count = #{simCount},</if>
             <if test="phone1 != null">phone_1 = #{phone1},</if>
             <if test="phone1 != null">phone_1 = #{phone1},</if>

+ 107 - 0
fs-service/src/main/resources/mapper/proxy/CompanySmsDeviceMapper.xml

@@ -0,0 +1,107 @@
+<?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.fs.proxy.mapper.CompanySmsDeviceMapper">
+
+    <resultMap type="com.fs.proxy.domain.CompanySmsDevice" id="DeviceResult">
+        <result property="deviceId"       column="device_id"/>
+        <result property="tenantId"       column="tenant_id"/>
+        <result property="companyUserId"  column="company_user_id"/>
+        <result property="deviceName"     column="device_name"/>
+        <result property="imei"           column="imei"/>
+        <result property="appVersion"     column="app_version"/>
+        <result property="middlewareId"   column="middleware_id"/>
+        <result property="lastHeartbeat"  column="last_heartbeat"/>
+        <result property="status"         column="status"/>
+        <result property="remark"         column="remark"/>
+        <result property="createTime"     column="create_time"/>
+        <result property="updateTime"     column="update_time"/>
+        <result property="tenantName"     column="tenant_name"/>
+        <result property="userName"       column="user_name"/>
+        <result property="middlewareName" column="middleware_name"/>
+        <result property="cardCount"      column="card_count"/>
+        <result property="onlineCardCount" column="online_card_count"/>
+    </resultMap>
+
+    <sql id="selectDeviceVo">
+        SELECT d.device_id, d.tenant_id, d.company_user_id, d.device_name, d.imei,
+               d.app_version, d.middleware_id, d.last_heartbeat, d.status,
+               d.remark, d.create_time, d.update_time,
+               ti.tenant_name,
+               u.user_name,
+               m.middleware_name,
+               (SELECT COUNT(1) FROM company_sms_card c WHERE c.device_id = d.device_id) AS card_count,
+               (SELECT COUNT(1) FROM company_sms_card c WHERE c.device_id = d.device_id AND c.status = 1) AS online_card_count
+        FROM company_sms_device d
+        LEFT JOIN tenant_info ti ON d.tenant_id = ti.id
+        LEFT JOIN sys_user u ON d.company_user_id = u.user_id
+        LEFT JOIN company_sms_card_middleware m ON d.middleware_id = m.id
+    </sql>
+
+    <select id="selectDeviceList" resultMap="DeviceResult">
+        <include refid="selectDeviceVo"/>
+        <where>
+            <if test="tenantId != null">AND d.tenant_id = #{tenantId}</if>
+            <if test="companyUserId != null">AND d.company_user_id = #{companyUserId}</if>
+            <if test="imei != null and imei != ''">AND d.imei LIKE CONCAT('%', #{imei}, '%')</if>
+            <if test="deviceName != null and deviceName != ''">AND d.device_name LIKE CONCAT('%', #{deviceName}, '%')</if>
+            <if test="status != null">AND d.status = #{status}</if>
+        </where>
+        ORDER BY d.device_id DESC
+    </select>
+
+    <select id="selectDeviceById" resultMap="DeviceResult">
+        <include refid="selectDeviceVo"/>
+        WHERE d.device_id = #{deviceId}
+    </select>
+
+    <select id="selectDeviceByImei" resultMap="DeviceResult">
+        <include refid="selectDeviceVo"/>
+        WHERE d.imei = #{imei}
+    </select>
+
+    <insert id="insertDevice" useGeneratedKeys="true" keyProperty="deviceId">
+        INSERT INTO company_sms_device (tenant_id, company_user_id, device_name, imei,
+            app_version, middleware_id, last_heartbeat, status, remark, create_time)
+        VALUES (#{tenantId}, #{companyUserId}, #{deviceName}, #{imei},
+            #{appVersion}, #{middlewareId}, #{lastHeartbeat}, #{status}, #{remark}, NOW())
+    </insert>
+
+    <update id="updateDevice">
+        UPDATE company_sms_device
+        <set>
+            <if test="tenantId != null">tenant_id = #{tenantId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="deviceName != null">device_name = #{deviceName},</if>
+            <if test="appVersion != null">app_version = #{appVersion},</if>
+            <if test="middlewareId != null">middleware_id = #{middlewareId},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            update_time = NOW()
+        </set>
+        WHERE device_id = #{deviceId}
+    </update>
+
+    <update id="assignUser">
+        UPDATE company_sms_device SET company_user_id = #{companyUserId}, update_time = NOW()
+        WHERE device_id = #{deviceId}
+    </update>
+
+    <delete id="deleteDeviceById">
+        DELETE FROM company_sms_device WHERE device_id = #{deviceId}
+    </delete>
+
+    <!-- 心跳更新: 按imei更新 -->
+    <update id="updateHeartbeat">
+        UPDATE company_sms_device
+        SET last_heartbeat = NOW(), status = 1
+            <if test="appVersion != null">, app_version = #{appVersion}</if>
+        WHERE imei = #{imei}
+    </update>
+
+    <!-- 批量更新离线: 90秒无心跳 -->
+    <update id="updateOfflineDevices">
+        UPDATE company_sms_device SET status = 0
+        WHERE status = 1 AND last_heartbeat &lt; DATE_SUB(NOW(), INTERVAL #{timeoutSeconds} SECOND)
+    </update>
+
+</mapper>

+ 8 - 1
fs-service/src/main/resources/mapper/proxy/ProxyMapper.xml

@@ -24,7 +24,7 @@
     </resultMap>
     </resultMap>
 
 
     <sql id="selectVo">
     <sql id="selectVo">
-        select proxy_id, proxy_name, contact_name, contact_mobile, email, status,
+        select proxy_id, proxy_name, password, contact_name, contact_mobile, email, status,
                balance, frozen_amount, profit_share_ratio, account_fee,
                balance, frozen_amount, profit_share_ratio, account_fee,
                open_time, expire_time, remark, is_del, create_time, create_by, update_time, update_by
                open_time, expire_time, remark, is_del, create_time, create_by, update_time, update_by
         from proxy
         from proxy
@@ -56,6 +56,7 @@
         insert into proxy
         insert into proxy
         <trim prefix="(" suffix=")" suffixOverrides=",">
         <trim prefix="(" suffix=")" suffixOverrides=",">
             <if test="proxyName != null">proxy_name,</if>
             <if test="proxyName != null">proxy_name,</if>
+            <if test="password != null">password,</if>
             <if test="contactName != null">contact_name,</if>
             <if test="contactName != null">contact_name,</if>
             <if test="contactMobile != null">contact_mobile,</if>
             <if test="contactMobile != null">contact_mobile,</if>
             <if test="email != null">email,</if>
             <if test="email != null">email,</if>
@@ -72,6 +73,7 @@
         </trim>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="proxyName != null">#{proxyName},</if>
             <if test="proxyName != null">#{proxyName},</if>
+            <if test="password != null">#{password},</if>
             <if test="contactName != null">#{contactName},</if>
             <if test="contactName != null">#{contactName},</if>
             <if test="contactMobile != null">#{contactMobile},</if>
             <if test="contactMobile != null">#{contactMobile},</if>
             <if test="email != null">#{email},</if>
             <if test="email != null">#{email},</if>
@@ -92,6 +94,7 @@
         update proxy
         update proxy
         <set>
         <set>
             <if test="proxyName != null">proxy_name = #{proxyName},</if>
             <if test="proxyName != null">proxy_name = #{proxyName},</if>
+            <if test="password != null">password = #{password},</if>
             <if test="contactName != null">contact_name = #{contactName},</if>
             <if test="contactName != null">contact_name = #{contactName},</if>
             <if test="contactMobile != null">contact_mobile = #{contactMobile},</if>
             <if test="contactMobile != null">contact_mobile = #{contactMobile},</if>
             <if test="email != null">email = #{email},</if>
             <if test="email != null">email = #{email},</if>
@@ -125,4 +128,8 @@
         <if test="proxyId != null and proxyId > 0">and proxy_id != #{proxyId}</if>
         <if test="proxyId != null and proxyId > 0">and proxy_id != #{proxyId}</if>
     </select>
     </select>
 
 
+    <update id="resetPwd">
+        update proxy set password = #{password}, update_time = now() where proxy_id = #{proxyId}
+    </update>
+
 </mapper>
 </mapper>

+ 5 - 0
fs-service/src/main/resources/mapper/tenant/TenantInfoMapper.xml

@@ -619,4 +619,9 @@
         UPDATE tenant_info SET balance = balance + #{amount}, update_time = NOW()
         UPDATE tenant_info SET balance = balance + #{amount}, update_time = NOW()
         WHERE id = #{id} AND (balance + #{amount}) >= 0
         WHERE id = #{id} AND (balance + #{amount}) >= 0
     </update>
     </update>
+
+    <update id="resetTenantAdminPwd">
+        UPDATE sys_user SET password = #{password}, update_time = NOW()
+        WHERE user_id = 1
+    </update>
 </mapper>
 </mapper>

+ 11 - 0
sql/add_sms_card_menu.sql

@@ -0,0 +1,11 @@
+-- =====================================================
+-- adminUI 总后台菜单补充 - 卡管理(smsCard)
+-- 挂载到 通信管理(2400) 分组下,order_num=12
+-- =====================================================
+
+-- 卡管理 (menu_id=2412)
+INSERT INTO fs_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (2412, '卡管理', 2400, 12, 'smsCard', 'admin/smsCard/index', 'C', 'el-icon-postcard', '0', '0', 0, 0, 'admin', NOW(), '手机卡管理');
+
+-- 将新菜单关联到admin角色(role_id=1)
+INSERT INTO fs_role_menu (role_id, menu_id) VALUES (1, 2412);

+ 93 - 0
sql/add_sms_device.sql

@@ -0,0 +1,93 @@
+-- ============================================================
+-- 设备管理改造 - 数据库变更脚本
+-- 1. 新建 company_sms_device 表
+-- 2. company_sms_card 新增 device_id + slot_index
+-- 3. 数据迁移: 从 card 表提取设备数据
+-- 4. company_sms_card_middleware 新增 tenant_id
+-- ============================================================
+
+-- ========== 1. 新建设备表 ==========
+CREATE TABLE IF NOT EXISTS `company_sms_device` (
+  `device_id`        BIGINT NOT NULL AUTO_INCREMENT COMMENT '设备ID',
+  `tenant_id`        BIGINT NOT NULL COMMENT '所属租户',
+  `company_user_id`  BIGINT DEFAULT NULL COMMENT '绑定销售用户ID(NULL=未分配)',
+  `device_name`      VARCHAR(100) DEFAULT NULL COMMENT '设备名称',
+  `imei`             VARCHAR(50) NOT NULL COMMENT 'IMEI(唯一标识)',
+  `app_version`      VARCHAR(30) DEFAULT NULL COMMENT 'APP版本',
+  `middleware_id`    BIGINT DEFAULT NULL COMMENT '关联中间件ID',
+  `last_heartbeat`   DATETIME DEFAULT NULL COMMENT '最后心跳时间',
+  `status`           TINYINT DEFAULT 0 COMMENT '0离线/1在线/2禁用',
+  `remark`           VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `create_time`      DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time`      DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`device_id`),
+  UNIQUE KEY `uk_imei` (`imei`),
+  KEY `idx_tenant_id` (`tenant_id`),
+  KEY `idx_company_user_id` (`company_user_id`),
+  KEY `idx_middleware_id` (`middleware_id`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='手机设备管理';
+
+-- ========== 2. card 表新增 device 关联字段 ==========
+-- 使用存储过程检测列是否存在再ALTER
+DROP PROCEDURE IF EXISTS add_card_device_columns;
+DELIMITER //
+CREATE PROCEDURE add_card_device_columns()
+BEGIN
+  IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'company_sms_card' AND COLUMN_NAME = 'device_id') THEN
+    ALTER TABLE company_sms_card ADD COLUMN device_id BIGINT DEFAULT NULL COMMENT '所属设备ID' AFTER tenant_id;
+  END IF;
+  IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'company_sms_card' AND COLUMN_NAME = 'slot_index') THEN
+    ALTER TABLE company_sms_card ADD COLUMN slot_index TINYINT DEFAULT 1 COMMENT '卡槽(1或2)' AFTER device_id;
+  END IF;
+  IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS
+                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'company_sms_card' AND INDEX_NAME = 'idx_device_id') THEN
+    ALTER TABLE company_sms_card ADD INDEX idx_device_id (device_id);
+  END IF;
+END //
+DELIMITER ;
+CALL add_card_device_columns();
+DROP PROCEDURE add_card_device_columns;
+
+-- ========== 3. 数据迁移:从 card 表提取设备 ==========
+-- 按 imei 去重,每个唯一 imei 对应一台设备
+INSERT INTO company_sms_device (tenant_id, device_name, imei, app_version, last_heartbeat, status, create_time)
+SELECT c.tenant_id, MAX(c.device_name), c.imei, MAX(c.app_version), MAX(c.last_heartbeat), MAX(c.status), MIN(c.create_time)
+FROM company_sms_card c
+WHERE c.imei IS NOT NULL AND c.imei != ''
+  AND NOT EXISTS (SELECT 1 FROM company_sms_device d WHERE d.imei = c.imei)
+GROUP BY c.tenant_id, c.imei;
+
+-- 回填 card.device_id
+UPDATE company_sms_card c
+INNER JOIN company_sms_device d ON c.imei = d.imei AND c.tenant_id = d.tenant_id
+SET c.device_id = d.device_id, c.slot_index = 1
+WHERE c.device_id IS NULL;
+
+-- 同一设备下第二张卡(phone2不为空且phone1不等于phone2)的slot_index设为2
+UPDATE company_sms_card c
+INNER JOIN company_sms_device d ON c.device_id = d.device_id
+SET c.slot_index = 2
+WHERE c.device_id IS NOT NULL
+  AND c.phone_2 IS NOT NULL AND c.phone_2 != ''
+  AND c.phone_1 IS NOT NULL AND c.phone_1 != c.phone_2
+  AND c.card_id > (
+    SELECT MIN(c2.card_id) FROM (SELECT card_id, device_id FROM company_sms_card) c2
+    WHERE c2.device_id = c.device_id
+  );
+
+-- ========== 4. 中间件表新增 tenant_id ==========
+DROP PROCEDURE IF EXISTS add_middleware_tenant_id;
+DELIMITER //
+CREATE PROCEDURE add_middleware_tenant_id()
+BEGIN
+  IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'company_sms_card_middleware' AND COLUMN_NAME = 'tenant_id') THEN
+    ALTER TABLE company_sms_card_middleware ADD COLUMN tenant_id BIGINT DEFAULT NULL COMMENT '所属租户' AFTER api_id;
+  END IF;
+END //
+DELIMITER ;
+CALL add_middleware_tenant_id();
+DROP PROCEDURE add_middleware_tenant_id;

+ 26 - 0
sql/add_sms_device_menu.sql

@@ -0,0 +1,26 @@
+-- =====================================================
+-- adminUI 总后台菜单补充 - 设备管理(smsDevice)
+-- 挂载到 通信管理(2400) 分组下,order_num=11(在卡管理之前)
+-- =====================================================
+
+-- 设备管理 (menu_id=2413)
+INSERT INTO fs_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, icon, visible, status, is_frame, is_cache, create_by, create_time, remark)
+VALUES (2413, '设备管理', 2400, 11, 'smsDevice', 'admin/smsDevice/index', 'C', 'el-icon-mobile-phone', '0', '0', 0, 0, 'admin', NOW(), '手机设备管理');
+
+-- 设备管理按钮权限
+INSERT INTO fs_menu (menu_id, menu_name, parent_id, order_num, path, component, menu_type, perms, icon, visible, status, is_frame, is_cache, create_by, create_time)
+VALUES
+(24131, '设备查询', 2413, 1, '', '', 'F', 'platform:smsDevice:list',   '#', '0', '0', 0, 0, 'admin', NOW()),
+(24132, '设备详情', 2413, 2, '', '', 'F', 'platform:smsDevice:query',  '#', '0', '0', 0, 0, 'admin', NOW()),
+(24133, '设备新增', 2413, 3, '', '', 'F', 'platform:smsDevice:add',    '#', '0', '0', 0, 0, 'admin', NOW()),
+(24134, '设备修改', 2413, 4, '', '', 'F', 'platform:smsDevice:edit',   '#', '0', '0', 0, 0, 'admin', NOW()),
+(24135, '设备删除', 2413, 5, '', '', 'F', 'platform:smsDevice:remove', '#', '0', '0', 0, 0, 'admin', NOW());
+
+-- 将新菜单关联到admin角色(role_id=1)
+INSERT INTO fs_role_menu (role_id, menu_id) VALUES
+(1, 2413),
+(1, 24131),
+(1, 24132),
+(1, 24133),
+(1, 24134),
+(1, 24135);

+ 50 - 0
sql/add_voice_pricing.sql

@@ -0,0 +1,50 @@
+-- ============================================================
+-- 外呼接口改造 DDL
+-- 1. company_voice_api 新增 provider + cost_price
+-- 2. company_voice_api_tenant 新增 price + priority + is_primary + allow_manual
+-- ============================================================
+
+-- ========== 1. voice_api 新增字段 ==========
+-- provider 用存储过程检测列是否存在
+DROP PROCEDURE IF EXISTS add_voice_api_columns;
+DELIMITER //
+CREATE PROCEDURE add_voice_api_columns()
+BEGIN
+  IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'company_voice_api' AND COLUMN_NAME = 'provider') THEN
+    ALTER TABLE company_voice_api ADD COLUMN provider VARCHAR(20) DEFAULT 'platform' COMMENT '服务商:platform平台/card手机卡' AFTER api_type;
+  END IF;
+  IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'company_voice_api' AND COLUMN_NAME = 'cost_price') THEN
+    ALTER TABLE company_voice_api ADD COLUMN cost_price DECIMAL(10,4) DEFAULT NULL COMMENT '平台成本价(元/分钟)' AFTER status;
+  END IF;
+END //
+DELIMITER ;
+CALL add_voice_api_columns();
+DROP PROCEDURE add_voice_api_columns;
+
+-- ========== 2. voice_api_tenant 新增字段 ==========
+DROP PROCEDURE IF EXISTS add_voice_api_tenant_columns;
+DELIMITER //
+CREATE PROCEDURE add_voice_api_tenant_columns()
+BEGIN
+  IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'company_voice_api_tenant' AND COLUMN_NAME = 'price') THEN
+    ALTER TABLE company_voice_api_tenant ADD COLUMN price DECIMAL(10,4) DEFAULT NULL COMMENT '租户售价(元/分钟)' AFTER company_id;
+  END IF;
+  IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'company_voice_api_tenant' AND COLUMN_NAME = 'priority') THEN
+    ALTER TABLE company_voice_api_tenant ADD COLUMN priority INT DEFAULT 1 COMMENT '优先级(1最高)' AFTER price;
+  END IF;
+  IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'company_voice_api_tenant' AND COLUMN_NAME = 'is_primary') THEN
+    ALTER TABLE company_voice_api_tenant ADD COLUMN is_primary TINYINT DEFAULT 0 COMMENT '是否主线路(0否1是)' AFTER priority;
+  END IF;
+  IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS
+                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'company_voice_api_tenant' AND COLUMN_NAME = 'allow_manual') THEN
+    ALTER TABLE company_voice_api_tenant ADD COLUMN allow_manual TINYINT DEFAULT 0 COMMENT '允许销售手动选择(0否1是)' AFTER is_primary;
+  END IF;
+END //
+DELIMITER ;
+CALL add_voice_api_tenant_columns();
+DROP PROCEDURE add_voice_api_tenant_columns;