Browse Source

feat(douyin): 新增抖音用户信息监控功能

- 在BaseEntity中新增createId字段用于记录创建者ID
- 实现自动填充创建者ID的元对象处理器
- 新增抖音用户信息API接口updateMonitorState
- 迁移DouYinUtils工具类到API模块并实现用户信息获取功能
- 在前端界面添加直播监控、资料更新等状态控制组件
- 实现抖音用户信息的状态监控开关功能
- 添加数据权限控制支持部门和用户级别访问控制
- 重构后端服务层实现监控状态更新逻辑
- 集成定时任务实现抖音用户信息自动更新功能
JX.Li 2 weeks ago
parent
commit
50ed4c1ef7
23 changed files with 415 additions and 289 deletions
  1. 32 0
      nexo-api/nexo-api-module/pom.xml
  2. 16 0
      nexo-api/nexo-api-module/src/main/java/com/nexo/module/api/douyin/RemoteDouYinUserInfoService.java
  3. 12 2
      nexo-api/nexo-api-module/src/main/java/com/nexo/module/api/douyin/domain/NexoDouyinUserInfo.java
  4. 36 1
      nexo-api/nexo-api-module/src/main/java/com/nexo/module/api/douyin/utils/DouYinUtils.java
  5. 1 0
      nexo-api/pom.xml
  6. 3 2
      nexo-common/nexo-common-core/src/main/java/com/nexo/common/core/utils/OkHttpClientUtils.java
  7. 6 1
      nexo-common/nexo-common-core/src/main/java/com/nexo/common/core/web/domain/BaseEntity.java
  8. 19 5
      nexo-common/nexo-common-mybatis/src/main/java/com/nexo/common/mybatis/handler/CreateAndUpdateMetaObjectHandler.java
  9. 5 0
      nexo-example/nexo-model/pom.xml
  10. 8 1
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/controller/NexoDouyinUserInfoController.java
  11. 13 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/domain/UpdataStatusBo.java
  12. 35 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/dubbo/RemoteDouYinUserInfoServiceImpl.java
  13. 10 1
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/mapper/NexoDouyinUserInfoMapper.java
  14. 6 1
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/INexoDouyinUserInfoService.java
  15. 31 32
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/impl/NexoDouyinUserInfoServiceImpl.java
  16. 12 1
      nexo-example/nexo-model/src/main/resources/mapper/douyin/NexoDouyinUserInfoMapper.xml
  17. 1 1
      nexo-example/nexo-model/src/test/java/com/nexo/model/douyin/抖音测试工具类.java
  18. 6 0
      nexo-modules/nexo-job/pom.xml
  19. 25 225
      nexo-modules/nexo-job/src/main/java/com/nexo/job/service/SampleService.java
  20. 8 1
      nexo-ui/src/api/douyin/douyinUserInfo.js
  21. 1 0
      nexo-ui/src/assets/icons/svg/live.svg
  22. 1 0
      nexo-ui/src/assets/icons/svg/xiangqing.svg
  23. 128 15
      nexo-ui/src/views/module/douyin/douyinUserInfo/index.vue

+ 32 - 0
nexo-api/nexo-api-module/pom.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>com.nexo</groupId>
+        <artifactId>nexo-api</artifactId>
+        <version>1.8.2</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>nexo-api-module</artifactId>
+
+    <description>
+        nexo-api-module 应用服务接口模块
+    </description>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>com.nexo</groupId>
+            <artifactId>nexo-common-dict</artifactId>
+        </dependency>
+        <!-- RuoYi Common Core-->
+        <dependency>
+            <groupId>com.nexo</groupId>
+            <artifactId>nexo-common-core</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 16 - 0
nexo-api/nexo-api-module/src/main/java/com/nexo/module/api/douyin/RemoteDouYinUserInfoService.java

@@ -0,0 +1,16 @@
+package com.nexo.module.api.douyin;
+
+import com.nexo.module.api.douyin.domain.NexoDouyinUserInfo;
+
+import java.util.List;
+
+/**
+ * 抖音服务
+ *
+ * @author Lion Li
+ */
+public interface RemoteDouYinUserInfoService {
+
+    void updateDouYinUserInfo(NexoDouyinUserInfo nexoDouyinUserInfo);
+    List<NexoDouyinUserInfo> getUpdateInfoList(String secuid);
+}

+ 12 - 2
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/domain/NexoDouyinUserInfo.java → nexo-api/nexo-api-module/src/main/java/com/nexo/module/api/douyin/domain/NexoDouyinUserInfo.java

@@ -1,4 +1,4 @@
-package com.nexo.model.douyin.domain;
+package com.nexo.module.api.douyin.domain;
 
 import com.alibaba.excel.annotation.ExcelProperty;
 import com.baomidou.mybatisplus.annotation.TableField;
@@ -10,6 +10,8 @@ import com.nexo.common.excel.convert.ExcelDictConvert;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.util.List;
+
 
 /**
  * 抖音用户信息对象 nexo_douyin_user_info
@@ -24,11 +26,12 @@ public class NexoDouyinUserInfo extends BaseEntity {
 
     private static final long serialVersionUID = 1L;
 
+    @TableId(value = "id")
+    private Long id;
 
     /**
      * 用户id
      */
-    @TableId(value = "user_id")
     @ExcelProperty(value = "用户id")
     private String userId;
 
@@ -120,4 +123,11 @@ public class NexoDouyinUserInfo extends BaseEntity {
     @TableField(exist = false)
     private String userUrl;
 
+    private Boolean jobInfoStatus;
+    private Boolean jobLiveStatus;
+    private Boolean jobVideoStatus;
+    @TableField(exist = false)
+    private List<Long> ids;
+    private Long deptId;
+
 }

+ 36 - 1
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/utils/DouYinUtils.java → nexo-api/nexo-api-module/src/main/java/com/nexo/module/api/douyin/utils/DouYinUtils.java

@@ -1,5 +1,8 @@
-package com.nexo.model.douyin.utils;
+package com.nexo.module.api.douyin.utils;
 
+import com.alibaba.fastjson2.JSONObject;
+import com.nexo.common.core.utils.OkHttpClientUtils;
+import com.nexo.module.api.douyin.domain.NexoDouyinUserInfo;
 import lombok.extern.slf4j.Slf4j;
 
 import javax.script.Invocable;
@@ -15,6 +18,38 @@ import java.util.regex.Pattern;
 @Slf4j
 public class DouYinUtils {
 
+    public static NexoDouyinUserInfo getUserInfo(String secuid){
+        NexoDouyinUserInfo item = new NexoDouyinUserInfo();
+        String params = "?device_platform=webapp&aid=6383&channel=channel_pc_web&publish_video_strategy_type=2&source=channel_pc_web&sec_user_id=" + secuid + "&personal_center_strategy=1&profile_other_record_enable=1&land_to=1&update_version_code=170400&pc_client_type=1&pc_libra_divert=Windows&support_h265=0&support_dash=1&cpu_core_num=24&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=148.0.0.0&browser_online=true&engine_name=Blink&engine_version=148.0.0.0&os_name=Windows&os_version=10&device_memory=32&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7639313678160397875";
+        String aBogus = DouYinUtils.getABogus(params);
+        String url = "https://www.douyin.com/aweme/v1/web/user/profile/other/" + params + "&a_bogus=" + aBogus + "&verifyFp=verify_mp4xmi43_R2ZADT9W_IKha_4C4e_9b3a_ctEUtT2EsPOe&fp=verify_mp4xmi43_R2ZADT9W_IKha_4C4e_9b3a_ctEUtT2EsPOe";
+        HashMap<String, String> header = DouYinUtils.getHeader();
+        String reqStr = OkHttpClientUtils.doGet(url, null, header);
+        // 解析json
+        try {
+            JSONObject reqJson = JSONObject.parseObject(reqStr);
+            item.setUserId(reqJson.getJSONObject("user").getString("uid"));
+            item.setSecUid(reqJson.getJSONObject("user").getString("sec_uid"));
+            item.setUniqueId(reqJson.getJSONObject("user").getString("unique_id"));
+            item.setNickname(reqJson.getJSONObject("user").getString("nickname"));
+            item.setSignature(reqJson.getJSONObject("user").getString("signature"));
+            item.setIpLocation(reqJson.getJSONObject("user").getString("ip_location"));
+            item.setAvatar(reqJson.getJSONObject("user").getJSONObject("avatar_medium").getJSONArray("url_list").getString(0));
+            item.setLiveStatus(reqJson.getJSONObject("user").getLong("live_status"));
+            item.setUserAge(reqJson.getJSONObject("user").getLong("user_age"));
+            item.setTotalFavorited(reqJson.getJSONObject("user").getLong("total_favorited"));
+            item.setFollowerCount(reqJson.getJSONObject("user").getLong("follower_count"));
+            item.setFollowingCount(reqJson.getJSONObject("user").getLong("following_count"));
+            item.setAwemeCount(reqJson.getJSONObject("user").getLong("aweme_count"));
+            item.setCoverUrl(reqJson.getJSONObject("user").getJSONArray("cover_url").getJSONObject(0).getJSONArray("url_list").getString(0));
+            item.setRoomId(reqJson.getJSONObject("user").getString("room_id"));
+        } catch (Exception e) {
+            throw new RuntimeException("查询用户信息时解析失败");
+        }
+        return item;
+    }
+
+
     public static String getABogus(String prams) {
         // 创建脚本引擎管理器
         ScriptEngineManager manager = new ScriptEngineManager();

+ 1 - 0
nexo-api/pom.xml

@@ -12,6 +12,7 @@
         <module>nexo-api-bom</module>
         <module>nexo-api-system</module>
         <module>nexo-api-resource</module>
+        <module>nexo-api-module</module>
     </modules>
 
     <artifactId>nexo-api</artifactId>

+ 3 - 2
nexo-common/nexo-common-core/src/main/java/com/nexo/common/core/utils/OkHttpClientUtils.java

@@ -168,11 +168,12 @@ public class OkHttpClientUtils {
      * @param headers 请求头字段 {k1:v1, k2: v2, ...}
      * @return string
      */
-    public static String doPost(String url, Map<String, String> params, Map<String, String> headers) {
+    public static String doPost(String url, Map<String, ?> params, Map<String, String> headers) {
         FormBody.Builder builder = new FormBody.Builder();
         if (params != null && params.keySet().size() > 0) {
             for (String key : params.keySet()) {
-                builder.add(key, params.get(key));
+                log.info("key: {}, value: {}", key, params.get(key));
+                builder.add(key, String.valueOf(params.get(key)));
             }
         }
         Request.Builder requestBuilder = new Request.Builder();

+ 6 - 1
nexo-common/nexo-common-core/src/main/java/com/nexo/common/core/web/domain/BaseEntity.java

@@ -32,7 +32,12 @@ public class BaseEntity implements Serializable {
     private String searchValue;
 
     /**
-     * 创建者
+     * 创建者id
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Long createId;
+    /**
+     * 创建者名称
      */
     @TableField(fill = FieldFill.INSERT)
     private String createBy;

+ 19 - 5
nexo-common/nexo-common-mybatis/src/main/java/com/nexo/common/mybatis/handler/CreateAndUpdateMetaObjectHandler.java

@@ -10,6 +10,7 @@ import com.nexo.common.satoken.utils.LoginHelper;
 import com.nexo.system.api.model.LoginUser;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.util.ObjectUtils;
 
 import java.util.Date;
 
@@ -27,12 +28,13 @@ public class CreateAndUpdateMetaObjectHandler implements MetaObjectHandler {
         try {
             if (ObjectUtil.isNotNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity) {
                 BaseEntity baseEntity = (BaseEntity) metaObject.getOriginalObject();
-                Date current = ObjectUtil.isNotNull(baseEntity.getCreateTime())
-                    ? baseEntity.getCreateTime() : new Date();
+                Date current = ObjectUtil.isNotNull(baseEntity.getCreateTime()) ? baseEntity.getCreateTime() : new Date();
                 baseEntity.setCreateTime(current);
                 baseEntity.setUpdateTime(current);
-                String username = StringUtils.isNotBlank(baseEntity.getCreateBy())
-                    ? baseEntity.getCreateBy() : getLoginUsername();
+                String username = StringUtils.isNotBlank(baseEntity.getCreateBy()) ? baseEntity.getCreateBy() : getLoginUsername();
+                Long userId = !ObjectUtils.isEmpty(baseEntity.getCreateId()) ? baseEntity.getCreateId() : getLoginUserId();
+                // 当前已登录 且 创建人ID为空 则填充
+                baseEntity.setCreateId(userId);
                 // 当前已登录 且 创建人为空 则填充
                 baseEntity.setCreateBy(username);
                 // 当前已登录 且 更新人为空 则填充
@@ -70,10 +72,22 @@ public class CreateAndUpdateMetaObjectHandler implements MetaObjectHandler {
         try {
             loginUser = LoginHelper.getLoginUser();
         } catch (Exception e) {
-            log.warn("自动注入警告 => 用户未登录");
             return null;
         }
         return ObjectUtil.isNotNull(loginUser) ? loginUser.getUsername() : null;
     }
 
+    /**
+     * 获取登录用户名
+     */
+    private Long getLoginUserId() {
+        LoginUser loginUser;
+        try {
+            loginUser = LoginHelper.getLoginUser();
+        } catch (Exception e) {
+            return null;
+        }
+        return ObjectUtil.isNotNull(loginUser) ? loginUser.getUserId() : null;
+    }
+
 }

+ 5 - 0
nexo-example/nexo-model/pom.xml

@@ -80,6 +80,11 @@
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.nexo</groupId>
+            <artifactId>nexo-api-module</artifactId>
+            <version>1.8.2</version>
+        </dependency>
     </dependencies>
 
 

+ 8 - 1
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/controller/NexoDouyinUserInfoController.java

@@ -10,8 +10,9 @@ import com.nexo.common.log.annotation.Log;
 import com.nexo.common.log.enums.BusinessType;
 import com.nexo.common.mybatis.core.page.PageQuery;
 import com.nexo.common.mybatis.core.page.TableDataInfo;
-import com.nexo.model.douyin.domain.NexoDouyinUserInfo;
+import com.nexo.model.douyin.domain.UpdataStatusBo;
 import com.nexo.model.douyin.service.INexoDouyinUserInfoService;
+import com.nexo.module.api.douyin.domain.NexoDouyinUserInfo;
 import lombok.RequiredArgsConstructor;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
@@ -99,4 +100,10 @@ public class NexoDouyinUserInfoController extends BaseController {
     public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable String[] userIds) {
         return toAjax(iNexoDouyinUserInfoService.deleteWithValidByIds(Arrays.asList(userIds), true));
     }
+
+    @PostMapping("/updateMonitorState")
+    public R<Void> updateMonitorState(@RequestBody UpdataStatusBo item) {
+        return toAjax(iNexoDouyinUserInfoService.updateMonitorState(item));
+    }
+
 }

+ 13 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/domain/UpdataStatusBo.java

@@ -0,0 +1,13 @@
+package com.nexo.model.douyin.domain;
+
+import lombok.Data;
+
+@Data
+public class UpdataStatusBo {
+
+    private Long id;
+    private Boolean jobInfoStatus;
+    private Boolean jobLiveStatus;
+    private Boolean jobVideoStatus;
+
+}

+ 35 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/dubbo/RemoteDouYinUserInfoServiceImpl.java

@@ -0,0 +1,35 @@
+package com.nexo.model.douyin.dubbo;
+
+import com.nexo.model.douyin.service.INexoDouyinUserInfoService;
+import com.nexo.module.api.douyin.RemoteDouYinUserInfoService;
+import com.nexo.module.api.douyin.domain.NexoDouyinUserInfo;
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+
+/**
+ * 参数配置服务
+ *
+ * @author nexo
+ */
+
+@RequiredArgsConstructor
+@Service
+@DubboService
+public class RemoteDouYinUserInfoServiceImpl implements RemoteDouYinUserInfoService {
+
+    private final INexoDouyinUserInfoService userInfoService;
+
+    @Override
+    public void updateDouYinUserInfo(NexoDouyinUserInfo nexoDouyinUserInfo) {
+        userInfoService.updateBy(nexoDouyinUserInfo);
+    }
+
+    @Override
+    public List<NexoDouyinUserInfo> getUpdateInfoList(String secuid) {
+        return userInfoService.getUpdateInfoList(secuid);
+    }
+}

+ 10 - 1
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/mapper/NexoDouyinUserInfoMapper.java

@@ -1,7 +1,12 @@
 package com.nexo.model.douyin.mapper;
 
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
+import com.nexo.common.mybatis.annotation.DataColumn;
+import com.nexo.common.mybatis.annotation.DataPermission;
 import com.nexo.common.mybatis.core.mapper.BaseMapperPlus;
-import com.nexo.model.douyin.domain.NexoDouyinUserInfo;
+import com.nexo.module.api.douyin.domain.NexoDouyinUserInfo;
+
+import java.util.List;
 
 /**
  * 抖音用户信息Mapper接口
@@ -9,6 +14,10 @@ import com.nexo.model.douyin.domain.NexoDouyinUserInfo;
  * @author nexo
  * @date 2026-05-21
  */
+@DataPermission({@DataColumn(key = "deptName", value = "dept_id"), @DataColumn(key = "userName", value = "create_id")})
 public interface NexoDouyinUserInfoMapper extends BaseMapperPlus<NexoDouyinUserInfoMapper, NexoDouyinUserInfo, NexoDouyinUserInfo> {
 
+    @InterceptorIgnore(dataPermission = "true")  // 忽略数据权限
+    List<NexoDouyinUserInfo> getUpdateInfoList(String secuid);
+
 }

+ 6 - 1
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/INexoDouyinUserInfoService.java

@@ -2,7 +2,8 @@ package com.nexo.model.douyin.service;
 
 import com.nexo.common.mybatis.core.page.PageQuery;
 import com.nexo.common.mybatis.core.page.TableDataInfo;
-import com.nexo.model.douyin.domain.NexoDouyinUserInfo;
+import com.nexo.model.douyin.domain.UpdataStatusBo;
+import com.nexo.module.api.douyin.domain.NexoDouyinUserInfo;
 
 import java.util.Collection;
 import java.util.List;
@@ -44,4 +45,8 @@ public interface INexoDouyinUserInfoService {
      * 校验并批量删除抖音用户信息信息
      */
     Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid);
+
+    Boolean updateMonitorState(UpdataStatusBo item);
+
+    List<NexoDouyinUserInfo> getUpdateInfoList(String secuid);
 }

+ 31 - 32
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/impl/NexoDouyinUserInfoServiceImpl.java

@@ -1,6 +1,5 @@
 package com.nexo.model.douyin.service.impl;
 
-import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -8,18 +7,20 @@ import com.nexo.common.core.utils.OkHttpClientUtils;
 import com.nexo.common.core.utils.StringUtils;
 import com.nexo.common.mybatis.core.page.PageQuery;
 import com.nexo.common.mybatis.core.page.TableDataInfo;
-import com.nexo.model.douyin.domain.NexoDouyinUserInfo;
+import com.nexo.common.satoken.utils.LoginHelper;
+import com.nexo.model.douyin.domain.UpdataStatusBo;
 import com.nexo.model.douyin.mapper.NexoDouyinUserInfoMapper;
 import com.nexo.model.douyin.service.INexoDouyinUserInfoService;
-import com.nexo.model.douyin.utils.DouYinUtils;
+import com.nexo.module.api.douyin.domain.NexoDouyinUserInfo;
+import com.nexo.module.api.douyin.utils.DouYinUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -87,6 +88,7 @@ public class NexoDouyinUserInfoServiceImpl implements INexoDouyinUserInfoService
      * 新增抖音用户信息
      */
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public Boolean insertBy(NexoDouyinUserInfo item) {
         String string = DouYinUtils.getRegexString(item.getUserUrl(), "[a-zA-z]+://[^\\s]*", 0);
         String done = OkHttpClientUtils.doGetFollowRedirects(string, null);
@@ -100,36 +102,18 @@ public class NexoDouyinUserInfoServiceImpl implements INexoDouyinUserInfoService
         }
         String sec_user_id = DouYinUtils.getRegexString(mid, "user/(.*?)\\?", 0);
         sec_user_id = StringUtils.extractMiddleText(sec_user_id, "user/", "?");
-        String params = "?device_platform=webapp&aid=6383&channel=channel_pc_web&publish_video_strategy_type=2&source=channel_pc_web&sec_user_id=" + sec_user_id + "&personal_center_strategy=1&profile_other_record_enable=1&land_to=1&update_version_code=170400&pc_client_type=1&pc_libra_divert=Windows&support_h265=0&support_dash=1&cpu_core_num=24&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=148.0.0.0&browser_online=true&engine_name=Blink&engine_version=148.0.0.0&os_name=Windows&os_version=10&device_memory=32&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7639313678160397875";
-        String aBogus = DouYinUtils.getABogus(params);
-        String url = "https://www.douyin.com/aweme/v1/web/user/profile/other/" + params + "&a_bogus=" + aBogus + "&verifyFp=verify_mp4xmi43_R2ZADT9W_IKha_4C4e_9b3a_ctEUtT2EsPOe&fp=verify_mp4xmi43_R2ZADT9W_IKha_4C4e_9b3a_ctEUtT2EsPOe";
-        HashMap<String, String> header = DouYinUtils.getHeader();
-        String reqStr = OkHttpClientUtils.doGet(url, null, header);
-        // 解析json
-        try {
-            JSONObject reqJson = JSONObject.parseObject(reqStr);
-            item.setUserId(reqJson.getJSONObject("user").getString("uid"));
-            item.setSecUid(reqJson.getJSONObject("user").getString("sec_uid"));
-            item.setUniqueId(reqJson.getJSONObject("user").getString("unique_id"));
-            item.setNickname(reqJson.getJSONObject("user").getString("nickname"));
-            item.setSignature(reqJson.getJSONObject("user").getString("signature"));
-            item.setIpLocation(reqJson.getJSONObject("user").getString("ip_location"));
-            item.setAvatar(reqJson.getJSONObject("user").getJSONObject("avatar_medium").getJSONArray("url_list").getString(0));
-            item.setLiveStatus(reqJson.getJSONObject("user").getLong("live_status"));
-            item.setUserAge(reqJson.getJSONObject("user").getLong("user_age"));
-            item.setTotalFavorited(reqJson.getJSONObject("user").getLong("total_favorited"));
-            item.setFollowerCount(reqJson.getJSONObject("user").getLong("follower_count"));
-            item.setFollowingCount(reqJson.getJSONObject("user").getLong("following_count"));
-            item.setAwemeCount(reqJson.getJSONObject("user").getLong("aweme_count"));
-            item.setCoverUrl(reqJson.getJSONObject("user").getJSONArray("cover_url").getJSONObject(0).getJSONArray("url_list").getString(0));
-            item.setRoomId(reqJson.getJSONObject("user").getString("room_id"));
-        } catch (Exception e) {
-            throw new RuntimeException("查询用户信息时解析失败");
+        LambdaQueryWrapper<NexoDouyinUserInfo> lambdaed = Wrappers.lambdaQuery();
+        lambdaed.eq(NexoDouyinUserInfo::getSecUid, sec_user_id);
+        NexoDouyinUserInfo userInfo = baseMapper.selectVoOne(lambdaed);
+        if (userInfo != null) {
+            if (userInfo.getCreateBy().equals(LoginHelper.getUsername())) {
+                throw new RuntimeException("用户已存在");
+            }
+            return true;
         }
+        item = DouYinUtils.getUserInfo(sec_user_id);
+        item.setDeptId(LoginHelper.getDeptId());
         Boolean flag = baseMapper.insert(item) > 0;
-        if (flag) {
-            item.setUserId(item.getUserId());
-        }
         return flag;
     }
 
@@ -158,4 +142,19 @@ public class NexoDouyinUserInfoServiceImpl implements INexoDouyinUserInfoService
         }
         return baseMapper.deleteBatchIds(ids) > 0;
     }
+
+    @Override
+    public Boolean updateMonitorState(UpdataStatusBo item) {
+        NexoDouyinUserInfo userInfo = new NexoDouyinUserInfo();
+        userInfo.setId(item.getId());
+        userInfo.setJobInfoStatus(item.getJobInfoStatus());
+        userInfo.setJobLiveStatus(item.getJobLiveStatus());
+        userInfo.setJobVideoStatus(item.getJobVideoStatus());
+        return baseMapper.updateById(userInfo) > 0;
+    }
+
+    @Override
+    public List<NexoDouyinUserInfo> getUpdateInfoList(String secuid) {
+        return baseMapper.getUpdateInfoList(secuid);
+    }
 }

+ 12 - 1
nexo-example/nexo-model/src/main/resources/mapper/douyin/NexoDouyinUserInfoMapper.xml

@@ -4,7 +4,7 @@
         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.nexo.model.douyin.mapper.NexoDouyinUserInfoMapper">
 
-    <resultMap type="com.nexo.model.douyin.domain.NexoDouyinUserInfo" id="NexoDouyinUserInfoResult">
+    <resultMap type="com.nexo.module.api.douyin.domain.NexoDouyinUserInfo" id="NexoDouyinUserInfoResult">
         <result property="userId" column="user_id"/>
         <result property="secUid" column="sec_uid"/>
         <result property="uniqueId" column="unique_id"/>
@@ -22,5 +22,16 @@
         <result property="roomId" column="room_id"/>
     </resultMap>
 
+    <select id="getUpdateInfoList" resultType="com.nexo.module.api.douyin.domain.NexoDouyinUserInfo">
+        SELECT * FROM `nexo_douyin_user_info` WHERE job_info_status = 1
+        <where>
+            <if test="secuid != null">
+                AND sec_uid = #{secuid}
+            </if>
+            <if test="secuid == null">
+                group by create_by
+            </if>
+        </where>
+    </select>
 
 </mapper>

+ 1 - 1
nexo-example/nexo-model/src/test/java/com/nexo/model/douyin/抖音测试工具类.java

@@ -4,7 +4,7 @@ import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.nexo.common.core.utils.OkHttpClientUtils;
 import com.nexo.common.core.utils.StringUtils;
-import com.nexo.model.douyin.utils.DouYinUtils;
+import com.nexo.module.api.douyin.utils.DouYinUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;

+ 6 - 0
nexo-modules/nexo-job/pom.xml

@@ -76,6 +76,12 @@
             <artifactId>nexo-api-system</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.nexo</groupId>
+            <artifactId>nexo-api-module</artifactId>
+            <version>1.8.2</version>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 25 - 225
nexo-modules/nexo-job/src/main/java/com/nexo/job/service/SampleService.java

@@ -1,18 +1,17 @@
 package com.nexo.job.service;
 
-import com.xxl.job.core.context.XxlJobHelper;
+import com.alibaba.fastjson2.JSONObject;
+import com.nexo.module.api.douyin.RemoteDouYinUserInfoService;
+import com.nexo.module.api.douyin.domain.NexoDouyinUserInfo;
+import com.nexo.module.api.douyin.utils.DouYinUtils;
+import com.xxl.job.core.context.XxlJobContext;
 import com.xxl.job.core.handler.annotation.XxlJob;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
 import org.springframework.stereotype.Service;
 
-import java.io.BufferedInputStream;
-import java.io.BufferedReader;
-import java.io.DataOutputStream;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.Arrays;
-import java.util.concurrent.TimeUnit;
+import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * XxlJob开发示例(Bean模式)
@@ -29,225 +28,26 @@ import java.util.concurrent.TimeUnit;
 @Service
 public class SampleService {
 
+    @DubboReference
+    private RemoteDouYinUserInfoService remoteDouYinUserInfoService;
 
     /**
-     * 1、简单任务示例(Bean模式)
+     * 抖音直播监控
      */
-    @XxlJob("demoJobHandler")
-    public void demoJobHandler() throws Exception {
-        XxlJobHelper.log("XXL-JOB, Hello World.");
-
-        for (int i = 0; i < 5; i++) {
-            XxlJobHelper.log("beat at:" + i);
-        }
-        // default success
-    }
-
-
-    /**
-     * 2、分片广播任务
-     */
-    @XxlJob("shardingJobHandler")
-    public void shardingJobHandler() throws Exception {
-
-        // 分片参数
-        int shardIndex = XxlJobHelper.getShardIndex();
-        int shardTotal = XxlJobHelper.getShardTotal();
-
-        XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal);
-
-        // 业务逻辑
-        for (int i = 0; i < shardTotal; i++) {
-            if (i == shardIndex) {
-                XxlJobHelper.log("第 {} 片, 命中分片开始处理", i);
-            } else {
-                XxlJobHelper.log("第 {} 片, 忽略", i);
-            }
-        }
-
+    @XxlJob("douyinLiveMonitor")
+    public void douyinLiveMonitor() throws Exception {
+        List<NexoDouyinUserInfo> updateInfoList = remoteDouYinUserInfoService.getUpdateInfoList(null);
+        for (int i = 0; i < updateInfoList.size(); i++) {
+            NexoDouyinUserInfo info = updateInfoList.get(i);
+            List<String> strings = remoteDouYinUserInfoService.getUpdateInfoList(info.getSecUid()).stream().map(item -> item.getSecUid()).collect(Collectors.toList());
+
+        }
+        // 通过上下文获取任务参数
+        String param = XxlJobContext.getXxlJobContext().getJobParam();
+        JSONObject parsed = JSONObject.parseObject(param);
+        String sec_uid = parsed.getString("sec_uid");
+        NexoDouyinUserInfo userInfo = DouYinUtils.getUserInfo(sec_uid);
+        remoteDouYinUserInfoService.updateDouYinUserInfo(userInfo);
     }
 
-
-    /**
-     * 3、命令行任务
-     */
-    @XxlJob("commandJobHandler")
-    public void commandJobHandler() throws Exception {
-        String command = XxlJobHelper.getJobParam();
-        int exitValue = -1;
-
-        BufferedReader bufferedReader = null;
-        try {
-            // command process
-            ProcessBuilder processBuilder = new ProcessBuilder();
-            processBuilder.command(command);
-            processBuilder.redirectErrorStream(true);
-
-            Process process = processBuilder.start();
-            //Process process = Runtime.getRuntime().exec(command);
-
-            BufferedInputStream bufferedInputStream = new BufferedInputStream(process.getInputStream());
-            bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream));
-
-            // command log
-            String line;
-            while ((line = bufferedReader.readLine()) != null) {
-                XxlJobHelper.log(line);
-            }
-
-            // command exit
-            process.waitFor();
-            exitValue = process.exitValue();
-        } catch (Exception e) {
-            XxlJobHelper.log(e);
-        } finally {
-            if (bufferedReader != null) {
-                bufferedReader.close();
-            }
-        }
-
-        if (exitValue == 0) {
-            // default success
-        } else {
-            XxlJobHelper.handleFail("command exit value(" + exitValue + ") is failed");
-        }
-
-    }
-
-
-    /**
-     * 4、跨平台Http任务
-     * 参数示例:
-     * "url: http://www.baidu.com\n" +
-     * "method: get\n" +
-     * "data: content\n";
-     */
-    @XxlJob("httpJobHandler")
-    public void httpJobHandler() throws Exception {
-
-        // param parse
-        String param = XxlJobHelper.getJobParam();
-        if (param == null || param.trim().length() == 0) {
-            XxlJobHelper.log("param[" + param + "] invalid.");
-
-            XxlJobHelper.handleFail();
-            return;
-        }
-
-        String[] httpParams = param.split("\n");
-        String url = null;
-        String method = null;
-        String data = null;
-        for (String httpParam : httpParams) {
-            if (httpParam.startsWith("url:")) {
-                url = httpParam.substring(httpParam.indexOf("url:") + 4).trim();
-            }
-            if (httpParam.startsWith("method:")) {
-                method = httpParam.substring(httpParam.indexOf("method:") + 7).trim().toUpperCase();
-            }
-            if (httpParam.startsWith("data:")) {
-                data = httpParam.substring(httpParam.indexOf("data:") + 5).trim();
-            }
-        }
-
-        // param valid
-        if (url == null || url.trim().length() == 0) {
-            XxlJobHelper.log("url[" + url + "] invalid.");
-
-            XxlJobHelper.handleFail();
-            return;
-        }
-        if (method == null || !Arrays.asList("GET", "POST").contains(method)) {
-            XxlJobHelper.log("method[" + method + "] invalid.");
-
-            XxlJobHelper.handleFail();
-            return;
-        }
-        boolean isPostMethod = method.equals("POST");
-
-        // request
-        HttpURLConnection connection = null;
-        BufferedReader bufferedReader = null;
-        try {
-            // connection
-            URL realUrl = new URL(url);
-            connection = (HttpURLConnection) realUrl.openConnection();
-
-            // connection setting
-            connection.setRequestMethod(method);
-            connection.setDoOutput(isPostMethod);
-            connection.setDoInput(true);
-            connection.setUseCaches(false);
-            connection.setReadTimeout(5 * 1000);
-            connection.setConnectTimeout(3 * 1000);
-            connection.setRequestProperty("connection", "Keep-Alive");
-            connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
-            connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8");
-
-            // do connection
-            connection.connect();
-
-            // data
-            if (isPostMethod && data != null && data.trim().length() > 0) {
-                DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
-                dataOutputStream.write(data.getBytes("UTF-8"));
-                dataOutputStream.flush();
-                dataOutputStream.close();
-            }
-
-            // valid StatusCode
-            int statusCode = connection.getResponseCode();
-            if (statusCode != 200) {
-                throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid.");
-            }
-
-            // result
-            bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
-            StringBuilder result = new StringBuilder();
-            String line;
-            while ((line = bufferedReader.readLine()) != null) {
-                result.append(line);
-            }
-            String responseMsg = result.toString();
-
-            XxlJobHelper.log(responseMsg);
-
-            return;
-        } catch (Exception e) {
-            XxlJobHelper.log(e);
-
-            XxlJobHelper.handleFail();
-            return;
-        } finally {
-            try {
-                if (bufferedReader != null) {
-                    bufferedReader.close();
-                }
-                if (connection != null) {
-                    connection.disconnect();
-                }
-            } catch (Exception e2) {
-                XxlJobHelper.log(e2);
-            }
-        }
-
-    }
-
-    /**
-     * 5、生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑;
-     */
-    @XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy")
-    public void demoJobHandler2() throws Exception {
-        XxlJobHelper.log("XXL-JOB, Hello World.");
-    }
-
-    public void init() {
-        log.info("init");
-    }
-
-    public void destroy() {
-        log.info("destory");
-    }
-
-
 }

+ 8 - 1
nexo-ui/src/api/douyin/douyinUserInfo.js

@@ -25,7 +25,14 @@ export function addDouyinUserInfo(data) {
     data: data
   })
 }
-
+// 更新监控状态
+export function updateMonitorState(data) {
+  return request({
+    url: '/model/douyin/userInfo/updateMonitorState',
+    method: 'post',
+    data: data
+  })
+}
 // 修改抖音用户信息
 export function updateDouyinUserInfo(data) {
   return request({

+ 1 - 0
nexo-ui/src/assets/icons/svg/live.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1779431660756" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9791" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M768 354.986667l82.773333-51.712A85.333333 85.333333 0 0 1 981.333333 375.637333v308.053334a85.333333 85.333333 0 0 1-136.704 68.138666l-77.098666-58.112A128 128 0 0 1 640 810.666667H170.666667a128 128 0 0 1-128-128V341.333333a128 128 0 0 1 128-128h469.333333a128 128 0 0 1 128 128v13.653334z m0 100.693333v131.541333l128 96.426667V375.68l-128 80z m-85.333333-26.368V341.333333a42.666667 42.666667 0 0 0-42.666667-42.666666H170.666667a42.666667 42.666667 0 0 0-42.666667 42.666666v341.333334a42.666667 42.666667 0 0 0 42.666667 42.666666h469.333333a42.666667 42.666667 0 0 0 42.666667-42.666666v-247.808a42.666667 42.666667 0 0 1 0-5.546667z" p-id="9792"></path></svg>

+ 1 - 0
nexo-ui/src/assets/icons/svg/xiangqing.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1779431614023" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7792" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 960A448 448 0 1 1 512 64a448 448 0 0 1 0 896zM512 153.6a358.4 358.4 0 1 0 0 716.8A358.4 358.4 0 0 0 512 153.6z m0 627.2a44.8 44.8 0 0 1-44.8-44.8V422.4a44.8 44.8 0 1 1 89.6 0v313.6a44.8 44.8 0 0 1-44.8 44.8z m0-448a44.8 44.8 0 1 1-0.064-89.536A44.8 44.8 0 0 1 512 332.8z" p-id="7793"></path></svg>

+ 128 - 15
nexo-ui/src/views/module/douyin/douyinUserInfo/index.vue

@@ -68,18 +68,22 @@
       </el-row>
       <Waterfall :list="douyinUserInfoList" :width="310" :gutter="12">
         <template #item="{ item }">
-          <el-card shadow="never" :style="{ backgroundImage: `url(${item.coverUrl})` }" class="card-back">
+          <el-card shadow="never" :class="item.liveStatus === 0 ?'':'live'"
+                   :style="{ backgroundImage: `url(${item.coverUrl})` }" class="card-back"
+          >
             <div class="cover">
-              <div class="delete-but" @click="handleDelete(item)">
-                <svg-icon icon-class="delete"/>
+              <div class="handle-but">
+                <svg-icon @click="handleDelete(item)" icon-class="live"/>
+                <svg-icon @click="handleDelete(item)" icon-class="xiangqing"/>
+                <svg-icon @click="handleDelete(item)" icon-class="delete"/>
               </div>
-              <div class="no_one">
+              <div class="no_one mt20">
                 <div>
                   <el-image :src="item.avatar" :preview-src-list="[item.avatar]"
-                            style="width: 50px;border-radius: 50%" :class="item.liveStatus === 0 ?'':'live'"
+                            style="width: 50px;border-radius: 50%"
                   >
                   </el-image>
-                  <span v-if="item.liveStatus === 1" class="live-text">直播中</span>
+                  <span v-if="item.liveStatus === 1" class="live-text">直播中...</span>
 
                 </div>
                 <div class="user-info">
@@ -111,6 +115,41 @@
                   <span class="fs12 disabled user-info-count">粉丝<span>{{ item.followerCount }}</span></span>
                 </div>
               </div>
+              <div class="mt10 box-card">
+                <div class="box-card-but">
+                  <span>资料更新</span>
+                  <span :class="item.jobInfoStatus ? 'moitor-t' : 'moitor-f'" class="mt-2"
+                  >{{ item.jobInfoStatus ? '监控中' : '未开启' }}</span>
+                  <div :class="item.jobInfoStatus ? 'moitor-t-but' : 'moitor-f-but'" class="mt5"
+                       @click="updataStatus(item,'jobInfoStatus')"
+                  >
+                    {{ item.jobInfoStatus ? '关闭' : '开启' }}
+                  </div>
+                </div>
+                <div class="box-card-but">
+                  <span>直播监控</span>
+                  <span :class="item.jobInfoStatus ? 'moitor-t' : 'moitor-f'" class="mt-2"
+                  >{{ item.jobInfoStatus ? '监控中' : '未开启' }}</span>
+                  <div :class="item.jobInfoStatus ? 'moitor-t-but' : 'moitor-f-but'" class="mt5"
+                       @click="updataStatus(item,'jobInfoStatus')"
+                  >
+                    {{ item.jobInfoStatus ? '关闭' : '开启' }}
+                  </div>
+                </div>
+                <div class="box-card-but">
+                  <span>直播录制</span>
+                  <span :class="item.jobVideoStatus ? 'moitor-t' : 'moitor-f'" class="mt-2"
+                  >{{ item.jobVideoStatus ? '监控中' : '未开启' }}</span>
+                  <div :class="item.jobVideoStatus ? 'moitor-t-but' : 'moitor-f-but'" class="mt5"
+                       @click="updataStatus(item,'jobVideoStatus')"
+                  >
+                    {{ item.jobVideoStatus ? '关闭' : '开启' }}
+                  </div>
+                </div>
+              </div>
+              <div class="mt10 box-card">
+                数据更新时间:{{ item.updateTime }}
+              </div>
             </div>
           </el-card>
         </template>
@@ -145,7 +184,7 @@ import {
   getDouyinUserInfo,
   delDouyinUserInfo,
   addDouyinUserInfo,
-  updateDouyinUserInfo
+  updateDouyinUserInfo, updateMonitorState
 } from '@/api/douyin/douyinUserInfo'
 
 import { LazyImg, Waterfall } from 'vue-waterfall-plugin'
@@ -209,6 +248,16 @@ export default {
     this.getList()
   },
   methods: {
+    updataStatus(item, key) {
+      let data = {
+        id: item.id,
+        [key]: !item[key]
+      }
+      updateMonitorState(data).then(response => {
+        this.$modal.msgSuccess('修改成功')
+        this.getList()
+      })
+    },
     /** 查询抖音用户信息列表 */
     getList() {
       this.loading = true
@@ -226,7 +275,7 @@ export default {
     // 表单重置
     reset() {
       this.form = {
-        userUrl: '长按复制此条消息,开抖音搜索,查看TA的更多作品。 https://v.douyin.com/MQHBwAkCYew/'
+        userUrl: '长按复制此条消息,开抖音搜索,查看TA的更多作品。 https://v.douyin.com/MQHBwAkCYew/'
       }
       this.resetForm('form')
     },
@@ -291,7 +340,7 @@ export default {
     },
     /** 删除按钮操作 */
     handleDelete(row) {
-      const userIds = row.userId || this.ids
+      const userIds = row.id || this.ids
       this.$modal.confirm('是否确认删除抖音用户信息为"' + row.nickname + '"的数据项?').then(() => {
         this.loading = true
         return delDouyinUserInfo(userIds)
@@ -323,17 +372,22 @@ export default {
 </script>
 <style lang="scss" scoped>
 .card-back {
-  background: #304661;
-  background-size: contain;
+  background-size: cover;
   background-repeat: no-repeat;
   position: relative;
   overflow: hidden;
   border: #e5e5e5 solid 1px;
+  cursor: pointer;
 
   ::v-deep .el-card__body {
     padding: 0 !important;
   }
 
+  &:hover {
+    box-shadow: 0 0 10px #cccccc;
+    transform: translateY(-5px);
+  }
+
   &::before {
     content: '';
     position: absolute;
@@ -385,10 +439,11 @@ export default {
   color: #fff;
   font-size: 10px;
   padding: 1px 3px;
+  padding-bottom: 2px;
   border-radius: 3px;
   position: absolute;
-  top: 55px;
-  left: 27px;
+  top: 10px;
+  left: 20px;
 }
 
 @keyframes live-border-glow {
@@ -431,12 +486,70 @@ export default {
   }
 }
 
-.delete-but {
+.handle-but {
   position: absolute;
   right: 10px;
   top: 10px;
   z-index: 1;
   cursor: pointer;
-  color: #fe346e;
+  display: flex;
+  gap: 10px;
+  color: #0138c6;
+}
+
+.box-card {
+  border: #ffffff50 solid 1px;
+  background-color: #ffffff30;
+  font-size: 12px;
+  min-height: 20px;
+  border-radius: 5px;
+  display: flex;
+  justify-content: space-around;
+  padding: 5px 10px;
 }
+
+.moitor-f {
+  color: #f55353;
+}
+
+.moitor-t {
+  color: #0138c6;
+}
+
+.moitor-f-but {
+  color: #ff4747;
+  width: 100%;
+  text-align: center;
+  border-radius: 3px;
+  background-color: #ff000010;
+  border: #f5535350 solid 1px;
+
+  &:hover {
+    color: #ffffff;
+    background-color: #ff4747;
+    border: #ff4747 solid 1px;
+  }
+}
+
+.moitor-t-but {
+  color: #0138c6;
+  width: 100%;
+  text-align: center;
+  border-radius: 3px;
+  background-color: #0138c620;
+  border: #0138c650 solid 1px;
+
+  &:hover {
+    color: #ffffff;
+    background-color: #0138c6;
+    border: #0138c6 solid 1px;
+  }
+}
+
+.box-card-but {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
 </style>