Browse Source

feat(douyin): 添加抖音视频解析功能和界面

- 新增DouYinBo实体类用于抖音视频数据传输
- 实现DouYinUtils工具类解析抖音视频信息
- 添加前端API接口调用抖音视频解析服务
- 创建抖音作品分析页面支持视频解析展示
- 集成视频播放组件并添加相关SVG图标
- 实现操作日志区分接口访问日志功能
- 添加自定义单选框组件优化界面交互
- 创建开放接口控制器和服务层结构
JX.Li 2 weeks ago
parent
commit
46a4b4131b
36 changed files with 1704 additions and 263 deletions
  1. 71 0
      nexo-api/nexo-api-module/src/main/java/com/nexo/module/api/douyin/utils/DouYinUtils.java
  2. 6 0
      nexo-api/nexo-api-system/src/main/java/com/nexo/system/api/domain/SysOperLog.java
  3. 1 1
      nexo-common/nexo-common-core/src/main/java/com/nexo/common/core/constant/UserConstants.java
  4. 5 0
      nexo-common/nexo-common-log/src/main/java/com/nexo/common/log/annotation/Log.java
  5. 2 0
      nexo-common/nexo-common-log/src/main/java/com/nexo/common/log/aspect/LogAspect.java
  6. 5 0
      nexo-common/nexo-common-log/src/main/java/com/nexo/common/log/event/OperLogEvent.java
  7. 37 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/controller/NexoDouyinVideoController.java
  8. 9 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/domain/DouYinBo.java
  9. 22 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/INexoDouyinVideoService.java
  10. 33 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/impl/INexoDouyinVideoServiceImpl.java
  11. 22 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/openApi/controller/NexoOpenApiController.java
  12. 10 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/openApi/domain/OpenApiBo.java
  13. 4 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/openApi/service/NexoOpenApiService.java
  14. 8 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/openApi/service/impl/NexoOpenApiServiceImpl.java
  15. 257 254
      nexo-example/nexo-model/src/test/java/com/nexo/model/douyin/抖音测试工具类.java
  16. 2 2
      nexo-modules/nexo-system/src/main/java/com/nexo/system/controller/SysOperlogController.java
  17. 1 1
      nexo-modules/nexo-system/src/main/java/com/nexo/system/service/ISysOperLogService.java
  18. 7 2
      nexo-modules/nexo-system/src/main/java/com/nexo/system/service/impl/SysOperLogServiceImpl.java
  19. 1 0
      nexo-modules/nexo-system/src/main/resources/mapper/system/SysOperLogMapper.xml
  20. 11 0
      nexo-ui/src/api/douyin/douyinVideo.js
  21. 5 2
      nexo-ui/src/api/system/operlog.js
  22. 130 0
      nexo-ui/src/views/components/radio/nexo-radio.vue
  23. 8 0
      nexo-ui/src/views/components/video/svg/fanhui.svg
  24. 16 0
      nexo-ui/src/views/components/video/svg/fenxiang.svg
  25. 1 0
      nexo-ui/src/views/components/video/svg/jingyin.svg
  26. 2 0
      nexo-ui/src/views/components/video/svg/pause.svg
  27. 8 0
      nexo-ui/src/views/components/video/svg/play.svg
  28. 8 0
      nexo-ui/src/views/components/video/svg/quanping.svg
  29. 0 0
      nexo-ui/src/views/components/video/svg/qxquanping.svg
  30. 1 0
      nexo-ui/src/views/components/video/svg/xiaochuang.svg
  31. 1 0
      nexo-ui/src/views/components/video/svg/yinliang.svg
  32. 575 0
      nexo-ui/src/views/components/video/video_play.vue
  33. 128 0
      nexo-ui/src/views/module/douyin/worksAanalysis/index.vue
  34. 3 1
      nexo-ui/src/views/monitor/interfacelog/index.vue
  35. 0 0
      nexo-ui/src/views/monitor/logininfor/index.vue
  36. 304 0
      nexo-ui/src/views/monitor/operlog/index.vue

+ 71 - 0
nexo-api/nexo-api-module/src/main/java/com/nexo/module/api/douyin/utils/DouYinUtils.java

@@ -2,6 +2,8 @@ package com.nexo.module.api.douyin.utils;
 
 import com.alibaba.fastjson2.JSONObject;
 import com.nexo.common.core.utils.OkHttpClientUtils;
+import com.nexo.common.core.utils.StringUtils;
+import com.nexo.common.redis.utils.RedisUtils;
 import com.nexo.module.api.douyin.domain.NexoDouyinUserInfo;
 import lombok.extern.slf4j.Slf4j;
 
@@ -11,6 +13,7 @@ import javax.script.ScriptEngineManager;
 import javax.script.ScriptException;
 import java.io.FileNotFoundException;
 import java.io.FileReader;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Matcher;
@@ -19,6 +22,74 @@ import java.util.regex.Pattern;
 @Slf4j
 public class DouYinUtils {
 
+    // 作品解析
+    public static Map<String, Object> parse(String url) {
+        try {
+            String sendUrl;
+            HashMap<String, String> hashMap = new HashMap<>();
+            hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
+            if (url.contains("v.douyin.com")) {
+                String string = DouYinUtils.getRegexString(url, "[a-zA-z]+://[^\\s]*", 0);
+                String done = OkHttpClientUtils.doGetFollowRedirects(string, hashMap);
+                sendUrl = StringUtils.extractMiddleText(done, "\"", "\"");
+            } else {
+                sendUrl = "https://www.iesdouyin.com/share/video/" + DouYinUtils.getRegexString(url, "\\d+", 0);
+            }
+            log.info("请求地址:{}", sendUrl);
+            String string2 = OkHttpClientUtils.doGetFollowRedirects(sendUrl, hashMap);
+            String right = StringUtils.right(string2, "window._ROUTER_DATA = ");
+            String left = StringUtils.left(right, "</script>");
+            JSONObject jsonObject = JSONObject.parseObject(DouYinUtils.decodeUnicode(left));
+            JSONObject data = jsonObject.getJSONObject("loaderData").getJSONObject("video_(id)/page");
+            data = data.getJSONObject("videoInfoRes").getJSONArray("item_list").getJSONObject(0);
+            log.info("{}", data);
+            HashMap<String, Object> map = new HashMap<>();
+            String sec_uid = data.getJSONObject("author").getString("sec_uid");
+            map.put("sec_uid", sec_uid);
+            String short_id = data.getJSONObject("author").getString("short_id");
+            map.put("short_id", short_id);
+            String nickname = data.getJSONObject("author").getString("nickname");
+            map.put("nickname", nickname);
+            String unique_id = data.getJSONObject("author").getString("unique_id");
+            map.put("unique_id", unique_id);
+            String signature = data.getJSONObject("author").getString("signature");
+            map.put("signature", signature);
+            String avatar = data.getJSONObject("author").getJSONObject("avatar_thumb").getJSONArray("url_list").getString(0);
+            map.put("avatar", avatar);
+
+            if (data.getInteger("aweme_type") == 4) {
+                String play_addr = data.getJSONObject("video").getJSONObject("play_addr").getJSONArray("url_list").getString(0);
+                String string = play_addr.replaceAll("playwm", "play");
+                String done = OkHttpClientUtils.doGetFollowRedirects(string, hashMap);
+                map.put("play_addr", RedisUtils.getCacheMapValue("sys_config","douyin_video_play") + StringUtils.extractMiddleText(done, "\"", "\""));
+                String cover = data.getJSONObject("video").getJSONObject("cover").getJSONArray("url_list").getString(0);
+                map.put("cover", cover);
+            } else if (data.getInteger("aweme_type") == 2) {
+                ArrayList<String> list = new ArrayList<>();
+                for (int i = 0; i < data.getJSONArray("images").size(); i++) {
+                    String imgs = data.getJSONArray("images").getJSONObject(i).getJSONArray("url_list").getString(0);
+                    list.add(imgs);
+                }
+                map.put("images", list);
+            }
+
+            String desc = data.getString("desc");
+            map.put("desc", desc);
+            Integer comment_count = data.getJSONObject("statistics").getInteger("comment_count");
+            Integer share_count = data.getJSONObject("statistics").getInteger("share_count");
+            Integer digg_count = data.getJSONObject("statistics").getInteger("digg_count");
+            Integer collect_count = data.getJSONObject("statistics").getInteger("collect_count");
+            map.put("comment_count", comment_count);
+            map.put("share_count", share_count);
+            map.put("digg_count", digg_count);
+            map.put("collect_count", collect_count);
+            return map;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    // 获取用户信息
     public static NexoDouyinUserInfo getUserInfo(String secuid, String path) {
         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";

+ 6 - 0
nexo-api/nexo-api-system/src/main/java/com/nexo/system/api/domain/SysOperLog.java

@@ -55,6 +55,12 @@ public class SysOperLog implements Serializable {
     @TableField(exist = false)
     private Integer[] businessTypes;
 
+    /**
+     * 是否接口访问日志
+     */
+    @ExcelProperty(value = "是否接口访问日志")
+    private Boolean isApi;
+
     /**
      * 请求方法
      */

+ 1 - 1
nexo-common/nexo-common-core/src/main/java/com/nexo/common/core/constant/UserConstants.java

@@ -138,7 +138,7 @@ public interface UserConstants {
     /**
      * 管理员ID
      */
-    Long ADMIN_ID = 1L;
+    Long ADMIN_ID = 100000L;
 
     /**
      * 管理员角色key

+ 5 - 0
nexo-common/nexo-common-log/src/main/java/com/nexo/common/log/annotation/Log.java

@@ -24,6 +24,11 @@ public @interface Log {
      */
     BusinessType businessType() default BusinessType.OTHER;
 
+    /**
+     * 是否接口访问日志
+     */
+    boolean isApi() default false;
+
     /**
      * 操作人类别
      */

+ 2 - 0
nexo-common/nexo-common-log/src/main/java/com/nexo/common/log/aspect/LogAspect.java

@@ -115,6 +115,8 @@ public class LogAspect {
         operLog.setTitle(log.title());
         // 设置操作人类别
         operLog.setOperatorType(log.operatorType().ordinal());
+        // 设置是否接口访问日志
+        operLog.setIsApi(log.isApi());
         // 是否需要保存request,参数和值
         if (log.isSaveRequestData()) {
             // 获取参数的信息,传入到数据库中。

+ 5 - 0
nexo-common/nexo-common-log/src/main/java/com/nexo/common/log/event/OperLogEvent.java

@@ -36,6 +36,11 @@ public class OperLogEvent implements Serializable {
      */
     private Integer[] businessTypes;
 
+    /**
+     * 是否接口访问日志
+     */
+    private Boolean isApi;
+
     /**
      * 请求方法
      */

+ 37 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/controller/NexoDouyinVideoController.java

@@ -0,0 +1,37 @@
+package com.nexo.model.douyin.controller;
+
+import com.nexo.common.core.domain.R;
+import com.nexo.common.core.web.controller.BaseController;
+import com.nexo.model.douyin.domain.DouYinBo;
+import com.nexo.model.douyin.service.INexoDouyinVideoService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 抖音视频控制器
+ * 前端访问路由地址为:/douyin/video
+ *
+ * @author nexo
+ * @date 2026-05-21
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/douyin/video")
+public class NexoDouyinVideoController extends BaseController {
+
+    private final INexoDouyinVideoService iNexoDouyinUserInfoService;
+
+    /**
+     * 抖音视频解析
+     */
+    @PostMapping("/parse")
+    public R parse(@RequestBody DouYinBo item) {
+        return iNexoDouyinUserInfoService.parse(item);
+    }
+
+}

+ 9 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/domain/DouYinBo.java

@@ -0,0 +1,9 @@
+package com.nexo.model.douyin.domain;
+
+import lombok.Data;
+
+@Data
+public class DouYinBo {
+    private Integer type;
+    private String videoUrl;
+}

+ 22 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/INexoDouyinVideoService.java

@@ -0,0 +1,22 @@
+package com.nexo.model.douyin.service;
+
+import com.nexo.common.core.domain.R;
+import com.nexo.model.douyin.domain.DouYinBo;
+
+/**
+ * 抖音视频Service接口
+ *
+ * @author nexo
+ * @date 2026-05-21
+ */
+public interface INexoDouyinVideoService {
+
+    /**
+     * 抖音视频解析
+     *
+     * @param item
+     * @return
+     */
+    R parse(DouYinBo item);
+
+}

+ 33 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/impl/INexoDouyinVideoServiceImpl.java

@@ -0,0 +1,33 @@
+package com.nexo.model.douyin.service.impl;
+
+import com.nexo.common.core.domain.R;
+import com.nexo.model.douyin.domain.DouYinBo;
+import com.nexo.model.douyin.service.INexoDouyinVideoService;
+import com.nexo.module.api.douyin.utils.DouYinUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * 抖音视频Service接口
+ *
+ * @author nexo
+ * @date 2026-05-21
+ */
+@Slf4j
+@Service
+public class INexoDouyinVideoServiceImpl implements INexoDouyinVideoService {
+
+    @Override
+    public R parse(DouYinBo item) {
+        // 单个视频
+        if (item.getType() == 0) {
+            return R.ok("解析成功", DouYinUtils.parse(item.getVideoUrl()));
+        }
+        // 批量视频
+        else if (item.getType() == 1) {
+            String[] split = item.getVideoUrl().split("\\n");
+
+        }
+        return null;
+    }
+}

+ 22 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/openApi/controller/NexoOpenApiController.java

@@ -0,0 +1,22 @@
+package com.nexo.model.openApi.controller;
+
+import com.nexo.common.core.web.controller.BaseController;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 开放接口控制器
+ * 前端访问路由地址为:/openapi/
+ *
+ * @author nexo
+ * @date 2026-05-21
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/openapi")
+public class NexoOpenApiController extends BaseController {
+
+}

+ 10 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/openApi/domain/OpenApiBo.java

@@ -0,0 +1,10 @@
+package com.nexo.model.openApi.domain;
+
+import lombok.Data;
+
+@Data
+public class OpenApiBo {
+
+    private String apiName;
+
+}

+ 4 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/openApi/service/NexoOpenApiService.java

@@ -0,0 +1,4 @@
+package com.nexo.model.openApi.service;
+
+public interface NexoOpenApiService {
+}

+ 8 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/openApi/service/impl/NexoOpenApiServiceImpl.java

@@ -0,0 +1,8 @@
+package com.nexo.model.openApi.service.impl;
+
+import com.nexo.model.openApi.service.NexoOpenApiService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class NexoOpenApiServiceImpl implements NexoOpenApiService {
+}

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

@@ -1,254 +1,257 @@
-//package com.nexo.model.douyin;
-//
-//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.module.api.douyin.utils.DouYinUtils;
-//import lombok.extern.slf4j.Slf4j;
-//import org.junit.jupiter.api.Test;
-//import org.springframework.boot.test.context.SpringBootTest;
-//
-//import java.io.IOException;
-//import java.io.UnsupportedEncodingException;
-//import java.net.URLDecoder;
-//import java.net.URLEncoder;
-//import java.util.HashMap;
-//
-//@Slf4j
-//@SpringBootTest
-//public class 抖音测试工具类 {
-//
-//    @Test
-//    public void 单个作品解析() {
-//        String 抖音视频链接 = "1.23 cAG:/ :0pm 12/29 A@G.IV p身材教程来啦~ # 剪辑教程  https://v.douyin.com/wssqLCJ9O4c/ 复制此链接,打开Dou音搜索,直接观看视频!";
-//        String 抖音链接 = "5.66 :6pm 01/27 h@o.Qk mDh:/ 手机全屏壁纸。# 性感 # 完美身材  https://v.douyin.com/qDWqlDX8ouQ/ 复制此链接,打开Dou音搜索,直接观看视频!";
-//        String string = DouYinUtils.getRegexString(抖音链接, "[a-zA-z]+://[^\\s]*", 0);
-//        HashMap<String, String> hashMap = new HashMap<>();
-//        hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
-//        String done = OkHttpClientUtils.doGetFollowRedirects(string, hashMap);
-//        String string1 = StringUtils.extractMiddleText(done, "\"", "\"");
-//        String string2 = OkHttpClientUtils.doGetFollowRedirects(string1, hashMap);
-//        String right = StringUtils.right(string2, "window._ROUTER_DATA = ");
-//        String left = StringUtils.left(right, "</script>");
-//        JSONObject jsonObject = JSONObject.parseObject(DouYinUtils.decodeUnicode(left));
-//        JSONObject data = jsonObject.getJSONObject("loaderData").getJSONObject("video_(id)/page");
-//        data = data.getJSONObject("videoInfoRes").getJSONArray("item_list").getJSONObject(0);
-//        log.info("{}", data);
-//        log.info("===================== 发布者 ========================");
-//        String sec_uid = data.getJSONObject("author").getString("sec_uid");
-//        log.info("sec_uid:{}", sec_uid);
-//        String short_id = data.getJSONObject("author").getString("short_id");
-//        log.info("short_id:{}", short_id);
-//        String nickname = data.getJSONObject("author").getString("nickname");
-//        log.info("nickname:{}", nickname);
-//        String signature = data.getJSONObject("author").getString("signature");
-//        log.info("signature:{}", signature);
-//
-//        log.info("===================== 信息 ========================");
-//        if (data.getInteger("aweme_type") == 4) {
-//            String play_addr = data.getJSONObject("video").getJSONObject("play_addr").getJSONArray("url_list").getString(0);
-//            log.info("play_addr:{}", play_addr.replaceAll("playwm", "play"));
-//            String cover = data.getJSONObject("video").getJSONObject("cover").getJSONArray("url_list").getString(0);
-//            log.info("cover:{}", cover);
-//        } else if (data.getInteger("aweme_type") == 2) {
-//            for (int i = 0; i < data.getJSONArray("images").size(); i++) {
-//                String imgs = data.getJSONArray("images").getJSONObject(i).getJSONArray("url_list").getString(0);
-//                log.info("img[{}]:{}", i + 1, imgs);
-//            }
-//        }
-//
-//        String desc = data.getString("desc");
-//        log.info("desc:{}", desc);
-//        Integer comment_count = data.getJSONObject("statistics").getInteger("comment_count");
-//        Integer share_count = data.getJSONObject("statistics").getInteger("share_count");
-//        Integer digg_count = data.getJSONObject("statistics").getInteger("digg_count");
-//        Integer collect_count = data.getJSONObject("statistics").getInteger("collect_count");
-//        log.info("comment_count:{}", comment_count);
-//        log.info("share_count:{}", share_count);
-//        log.info("digg_count:{}", digg_count);
-//        log.info("collect_count:{}", collect_count);
-//    }
-//
-//    @Test
-//    public void 获取用户作品列表() {
-//        String 分享连接 = "0- 长按复制此条消息,打开抖音搜索,查看TA的更多作品。 https://v.douyin.com/iz6uY8RfrJ8/ 7@5.com :2pm";
-//        String string = DouYinUtils.getRegexString(分享连接, "[a-zA-z]+://[^\\s]*", 0);
-//        String done = OkHttpClientUtils.doGetFollowRedirects(string, null);
-//        String mid = null;
-//        try {
-//            mid = URLDecoder.decode(StringUtils.extractMiddleText(done, "\"", "\""), "UTF-8");
-//        } catch (UnsupportedEncodingException e) {
-//            throw new RuntimeException(e);
-//        }
-//        String maxcursor = "";
-//        String sec_user_id = DouYinUtils.getRegexString(mid, "user/(.*?)\\?", 1);
-//        log.info("sec_user_id:{}", sec_user_id);
-//        Integer count = 0;
-//        while (true) {
-//            String params = "device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=" + sec_user_id + "&max_cursor=" + maxcursor + "&locate_item_id=7388761148507639094&locate_query=false&show_live_replay_strategy=1&need_time_list=" + "0" + "&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&update_version_code=170400&pc_client_type=1&version_code=290100&version_name=29.1.0&cookie_enabled=true&screen_width=1832&screen_height=314&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=126.0.0.0&browser_online=true&engine_name=Blink&engine_version=126.0.0.0&os_name=Android&os_version=6.0&cpu_core_num=12&device_memory=8&platform=Android&downlink=10&effective_type=4g&round_trip_time=150&webid=7347601222205687359&verifyFp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&fp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&msToken=MIOLSj1Hic1rlNKxvhty424gjUhagKo0ti6PK1s9uFfs0keS0miQ0metd3ZljkX0KjkQU_3nmJZm8tuYhMjIC1jntmfmuTRAfcZEbz1UzWZVqMDrHgg%3D";
-//            String aBogus = DouYinUtils.getABogus(params);
-//            String url = "https://www.douyin.com/aweme/v1/web/aweme/post/?" + params + "&a_bogus=" + aBogus + "&verifyFp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&fp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e";
-//            HashMap<String, String> header = DouYinUtils.getHeader();
-//            String reqStr = OkHttpClientUtils.doGet(url, null, header);
-//            // 解析json
-//            JSONObject reqJson = JSONObject.parseObject(reqStr);
-//            maxcursor = reqJson.getString("max_cursor");
-//            JSONArray awemeList = reqJson.getJSONArray("aweme_list");
-//            count += awemeList.size();
-//            log.info("maxcursor:{}", maxcursor);
-//            log.info("aweme_list:{}", awemeList.size());
-//            if (reqJson.getInteger("has_more") != 1) {
-//                break;
-//            }
-//        }
-//        log.info("count:{}", count);
-//
-//    }
-//
-//    @Test
-//    public void 获取用户信息() {
-//        String 分享连接 = "长按复制此条消息,打开抖音搜索,查看TA的更多作品。 https://v.douyin.com/MQHBwAkCYew/";
-//        String string = DouYinUtils.getRegexString(分享连接, "[a-zA-z]+://[^\\s]*", 0);
-//        String done = OkHttpClientUtils.doGetFollowRedirects(string, null);
-//        String mid = null;
-//        try {
-//            mid = URLDecoder.decode(StringUtils.extractMiddleText(done, "\"", "\""), "UTF-8");
-//        } catch (UnsupportedEncodingException e) {
-//            throw new RuntimeException(e);
-//        }
-//        log.info("mid:{}", mid);
-//        String sec_user_id = DouYinUtils.getRegexString(mid, "user/(.*?)\\?", 0);
-//        sec_user_id = StringUtils.extractMiddleText(sec_user_id, "user/", "?");
-//        log.info("sec_user_id:{}", sec_user_id);
-//        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";
-//        log.info("url:{}", url);
-//        HashMap<String, String> header = DouYinUtils.getHeader();
-//        String reqStr = OkHttpClientUtils.doGet(url, null, header);
-//        // 解析json
-//        JSONObject reqJson = JSONObject.parseObject(reqStr);
-//        log.info("reqJson:{}", reqJson);
-//
-//    }
-//
-//    @Test
-//    public void 获取抖音热榜() {
-//        String url = "https://so-landing.douyin.com/aweme/v1/hot/search/list/?aid=581610&detail_list=1&board_type=0&board_sub_type=&need_board_tab=true&need_covid_tab=false&version_code=32.3.0 ";
-//        String string = OkHttpClientUtils.doGet(url);
-//        JSONObject jsonObject = JSONObject.parseObject(string);
-//        JSONArray jsonArray = jsonObject.getJSONObject("data").getJSONArray("word_list");
-//        for (int i = 0; i < jsonArray.size(); i++) {
-//            JSONObject wordJson = jsonArray.getJSONObject(i);
-//            if (wordJson.containsKey("group_id")) {
-//                String url2 = null;
-//                try {
-//                    url2 = "https://so.douyin.com/s?is_from_mobile_home=1&search_entrance=aweme&enter_method=hot_list_page&innerWidth=430&innerHeight=932&reloadNavStart=1778841683198&is_no_width_reload=0&keyword=" + URLEncoder.encode(wordJson.getString("word"), "UTF-8") + "&gid=" + wordJson.getString("group_id");
-//                } catch (UnsupportedEncodingException e) {
-//                    throw new RuntimeException(e);
-//                }
-//                HashMap<String, String> hashMap = new HashMap<>();
-//                hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
-//
-//                String string2 = OkHttpClientUtils.doGet(url2, null, hashMap);
-//                String substring = string2.substring(string2.indexOf("let data = {\"business_data\":") + "let data = {\"business_data\":".length(), string2.indexOf(",\"render_info\""));
-//                JSONArray parsed = JSONObject.parseArray(substring);
-//                JSONObject object = new JSONObject();
-//                object.put("group_id", wordJson.getString("group_id"));
-//                object.put("word", wordJson.getString("word"));
-//                JSONArray array = new JSONArray();
-//                for (int i1 = 0; i1 < parsed.size(); i1++) {
-//                    String string1 = parsed.getJSONObject(i1).getJSONObject("data").getString("provider_doc_id_str");
-//                    if (parsed.getJSONObject(i1).getInteger("type") == 1) {
-//                        array.add("https://www.douyin.com/video/" + string1);
-//                    }
-//                }
-//                object.put("list", array);
-//                log.info("{}", object);
-//            }
-//
-//        }
-//
-//    }
-//
-//    @Test
-//    public void 获取直播视频流() {
-//        String url = "https://live.douyin.com/660321581729";
-//        HashMap<String, String> hashMap = new HashMap<>();
-//        hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
-//        String done = OkHttpClientUtils.doGet(url, null, hashMap);
-//        String roomId = DouYinUtils.getRegexString(done, "roomId[^\\d]*(\\d{15,})", 1);
-//        if (StringUtils.isEmpty(roomId)) {
-//            throw new RuntimeException("未找到房间号");
-//        }
-//        String url2 = "https://webcast.amemv.com/webcast/room/reflow/info/?type_id=0&live_id=1&room_id=" + roomId + "&sec_user_id=&app_id=1128";
-//        String string = OkHttpClientUtils.doGet(url2, null, hashMap);
-//        JSONObject data = JSONObject.parseObject(string);
-//        JSONObject stream = data.getJSONObject("data").getJSONObject("room").getJSONObject("stream_url");
-//        JSONObject resolutionName = stream.getJSONObject("resolution_name");
-//        log.info("========================== flv ==========================");
-//        JSONObject flv_pull_url = stream.getJSONObject("flv_pull_url");
-//        flv_pull_url.forEach((key, value) -> {
-//            String resolution = resolutionName.getString(key);
-//            log.info("{}:{}", resolution, value);
-//        });
-//
-//        log.info("========================== hls ==========================");
-//        JSONObject hls_pull_url_map = stream.getJSONObject("hls_pull_url_map");
-//        hls_pull_url_map.forEach((key, value) -> {
-//            String resolution = resolutionName.getString(key);
-//            log.info("{}:{}", resolution, value);
-//        });
-//
-//        log.info("========================== 主播信息 ==========================");
-//
-//        JSONObject owner = data.getJSONObject("data").getJSONObject("room").getJSONObject("owner");
-//        String avatar = owner.getJSONObject("avatar_large").getJSONArray("url_list").getString(0);
-//        log.info("头像:{}", avatar);
-//        log.info("昵称:{}", owner.getString("nickname"));
-//        log.info("签名:{}", owner.getString("signature"));
-//        Integer following_count = owner.getJSONObject("follow_info").getInteger("following_count");
-//        log.info("关注数:{}", following_count);
-//        Integer follower_count = owner.getJSONObject("follow_info").getInteger("follower_count");
-//        log.info("粉丝数:{}", follower_count);
-//        log.info("SECUID:{}", owner.getString("sec_uid"));
-//        log.info("抖音号:{}", owner.getString("display_id"));
-//
-//
-//    }
-//
-//    @Test
-//    public void 获取抖音弹幕() {
-//        String liveId = "561090451047";
-//        try {
-//
-//            // 设置工作目录
-//            String workDir = "C:\\Users\\Administrator\\Downloads\\DouyinLiveWebFetcher-main";
-//
-//            // 构建命令
-//            String command = "cmd /c start cmd /k \"python main.py --live_id=" + liveId + "\"";
-//
-//            System.out.println("执行命令: " + command);
-//
-//            // 使用 ProcessBuilder 执行命令
-//            ProcessBuilder processBuilder = new ProcessBuilder("cmd", "/c", command);
-//            processBuilder.directory(new java.io.File(workDir));
-//            // 合并错误流和输出流
-//            processBuilder.redirectErrorStream(true);
-//
-//            // 启动进程
-//            Process process = processBuilder.start();
-//            // 等待进程自然结束
-//            int exitCode = process.waitFor();
-//            System.out.println("进程退出码: " + exitCode);
-//
-//
-//        } catch (IOException e) {
-//            e.printStackTrace();
-//        } catch (InterruptedException e) {
-//            throw new RuntimeException(e);
-//        }
-//    }
-//
-//}
+package com.nexo.model.douyin;
+
+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.common.redis.utils.RedisUtils;
+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;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.HashMap;
+
+@Slf4j
+@SpringBootTest
+public class 抖音测试工具类 {
+
+    @Test
+    public void 单个作品解析() {
+        String 抖音视频链接 = "1.23 cAG:/ :0pm 12/29 A@G.IV p身材教程来啦~ # 剪辑教程  https://v.douyin.com/wssqLCJ9O4c/ 复制此链接,打开Dou音搜索,直接观看视频!";
+        String 抖音链接 = "5.66 :6pm 01/27 h@o.Qk mDh:/ 手机全屏壁纸。# 性感 # 完美身材  https://v.douyin.com/qDWqlDX8ouQ/ 复制此链接,打开Dou音搜索,直接观看视频!";
+        String string = DouYinUtils.getRegexString(抖音链接, "[a-zA-z]+://[^\\s]*", 0);
+        HashMap<String, String> hashMap = new HashMap<>();
+        hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
+        String done = OkHttpClientUtils.doGetFollowRedirects(string, hashMap);
+        String string1 = StringUtils.extractMiddleText(done, "\"", "\"");
+        String string2 = OkHttpClientUtils.doGetFollowRedirects(string1, hashMap);
+        String right = StringUtils.right(string2, "window._ROUTER_DATA = ");
+        String left = StringUtils.left(right, "</script>");
+        JSONObject jsonObject = JSONObject.parseObject(DouYinUtils.decodeUnicode(left));
+        JSONObject data = jsonObject.getJSONObject("loaderData").getJSONObject("video_(id)/page");
+        data = data.getJSONObject("videoInfoRes").getJSONArray("item_list").getJSONObject(0);
+        log.info("{}", data);
+        log.info("===================== 发布者 ========================");
+        String sec_uid = data.getJSONObject("author").getString("sec_uid");
+        log.info("sec_uid:{}", sec_uid);
+        String short_id = data.getJSONObject("author").getString("short_id");
+        log.info("short_id:{}", short_id);
+        String nickname = data.getJSONObject("author").getString("nickname");
+        log.info("nickname:{}", nickname);
+        String signature = data.getJSONObject("author").getString("signature");
+        log.info("signature:{}", signature);
+
+        log.info("===================== 信息 ========================");
+        if (data.getInteger("aweme_type") == 4) {
+            String play_addr = data.getJSONObject("video").getJSONObject("play_addr").getJSONArray("url_list").getString(0);
+            log.info("play_addr:{}", play_addr.replaceAll("playwm", "play"));
+            String cover = data.getJSONObject("video").getJSONObject("cover").getJSONArray("url_list").getString(0);
+            log.info("cover:{}", cover);
+        } else if (data.getInteger("aweme_type") == 2) {
+            for (int i = 0; i < data.getJSONArray("images").size(); i++) {
+                String imgs = data.getJSONArray("images").getJSONObject(i).getJSONArray("url_list").getString(0);
+                log.info("img[{}]:{}", i + 1, imgs);
+            }
+        }
+
+        String desc = data.getString("desc");
+        log.info("desc:{}", desc);
+        Integer comment_count = data.getJSONObject("statistics").getInteger("comment_count");
+        Integer share_count = data.getJSONObject("statistics").getInteger("share_count");
+        Integer digg_count = data.getJSONObject("statistics").getInteger("digg_count");
+        Integer collect_count = data.getJSONObject("statistics").getInteger("collect_count");
+        log.info("comment_count:{}", comment_count);
+        log.info("share_count:{}", share_count);
+        log.info("digg_count:{}", digg_count);
+        log.info("collect_count:{}", collect_count);
+    }
+
+    @Test
+    public void 获取用户作品列表() {
+        String 分享连接 = "0- 长按复制此条消息,打开抖音搜索,查看TA的更多作品。 https://v.douyin.com/iz6uY8RfrJ8/ 7@5.com :2pm";
+        String string = DouYinUtils.getRegexString(分享连接, "[a-zA-z]+://[^\\s]*", 0);
+        String done = OkHttpClientUtils.doGetFollowRedirects(string, null);
+        String mid = null;
+        try {
+            mid = URLDecoder.decode(StringUtils.extractMiddleText(done, "\"", "\""), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        String maxcursor = "";
+        String sec_user_id = DouYinUtils.getRegexString(mid, "user/(.*?)\\?", 1);
+        log.info("sec_user_id:{}", sec_user_id);
+        Integer count = 0;
+        while (true) {
+            String params = "device_platform=webapp&aid=6383&channel=channel_pc_web&sec_user_id=" + sec_user_id + "&max_cursor=" + maxcursor + "&locate_item_id=7388761148507639094&locate_query=false&show_live_replay_strategy=1&need_time_list=" + "0" + "&time_list_query=0&whale_cut_token=&cut_version=1&count=18&publish_video_strategy_type=2&update_version_code=170400&pc_client_type=1&version_code=290100&version_name=29.1.0&cookie_enabled=true&screen_width=1832&screen_height=314&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=126.0.0.0&browser_online=true&engine_name=Blink&engine_version=126.0.0.0&os_name=Android&os_version=6.0&cpu_core_num=12&device_memory=8&platform=Android&downlink=10&effective_type=4g&round_trip_time=150&webid=7347601222205687359&verifyFp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&fp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&msToken=MIOLSj1Hic1rlNKxvhty424gjUhagKo0ti6PK1s9uFfs0keS0miQ0metd3ZljkX0KjkQU_3nmJZm8tuYhMjIC1jntmfmuTRAfcZEbz1UzWZVqMDrHgg%3D";
+            String aBogus = DouYinUtils.getABogus(params, RedisUtils.getCacheMapValue("sys_config", "douyin_config_dev"));
+            String url = "https://www.douyin.com/aweme/v1/web/aweme/post/?" + params + "&a_bogus=" + aBogus + "&verifyFp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e&fp=verify_lwllt9d5_131z6m2c_JOGv_4TDi_Aoje_kOMojbHCPw0e";
+            HashMap<String, String> header = DouYinUtils.getHeader();
+            String reqStr = OkHttpClientUtils.doGet(url, null, header);
+            // 解析json
+            JSONObject reqJson = JSONObject.parseObject(reqStr);
+            maxcursor = reqJson.getString("max_cursor");
+            JSONArray awemeList = reqJson.getJSONArray("aweme_list");
+            count += awemeList.size();
+            log.info("maxcursor:{}", maxcursor);
+            log.info("aweme_list_size:{}", awemeList.size());
+            log.info("aweme:{}", awemeList.getJSONObject(0));
+            if (reqJson.getInteger("has_more") != 1) {
+                break;
+            }
+            break;
+        }
+        log.info("count:{}", count);
+
+    }
+
+    @Test
+    public void 获取用户信息() {
+        String 分享连接 = "长按复制此条消息,打开抖音搜索,查看TA的更多作品。 https://v.douyin.com/MQHBwAkCYew/";
+        String string = DouYinUtils.getRegexString(分享连接, "[a-zA-z]+://[^\\s]*", 0);
+        String done = OkHttpClientUtils.doGetFollowRedirects(string, null);
+        String mid = null;
+        try {
+            mid = URLDecoder.decode(StringUtils.extractMiddleText(done, "\"", "\""), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+        log.info("mid:{}", mid);
+        String sec_user_id = DouYinUtils.getRegexString(mid, "user/(.*?)\\?", 0);
+        sec_user_id = StringUtils.extractMiddleText(sec_user_id, "user/", "?");
+        log.info("sec_user_id:{}", sec_user_id);
+        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, RedisUtils.getCacheMapValue("sys_config", "douyin_config_dev"));
+        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";
+        log.info("url:{}", url);
+        HashMap<String, String> header = DouYinUtils.getHeader();
+        String reqStr = OkHttpClientUtils.doGet(url, null, header);
+        // 解析json
+        JSONObject reqJson = JSONObject.parseObject(reqStr);
+        log.info("reqJson:{}", reqJson);
+
+    }
+
+    @Test
+    public void 获取抖音热榜() {
+        String url = "https://so-landing.douyin.com/aweme/v1/hot/search/list/?aid=581610&detail_list=1&board_type=0&board_sub_type=&need_board_tab=true&need_covid_tab=false&version_code=32.3.0 ";
+        String string = OkHttpClientUtils.doGet(url);
+        JSONObject jsonObject = JSONObject.parseObject(string);
+        JSONArray jsonArray = jsonObject.getJSONObject("data").getJSONArray("word_list");
+        for (int i = 0; i < jsonArray.size(); i++) {
+            JSONObject wordJson = jsonArray.getJSONObject(i);
+            if (wordJson.containsKey("group_id")) {
+                String url2 = null;
+                try {
+                    url2 = "https://so.douyin.com/s?is_from_mobile_home=1&search_entrance=aweme&enter_method=hot_list_page&innerWidth=430&innerHeight=932&reloadNavStart=1778841683198&is_no_width_reload=0&keyword=" + URLEncoder.encode(wordJson.getString("word"), "UTF-8") + "&gid=" + wordJson.getString("group_id");
+                } catch (UnsupportedEncodingException e) {
+                    throw new RuntimeException(e);
+                }
+                HashMap<String, String> hashMap = new HashMap<>();
+                hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
+
+                String string2 = OkHttpClientUtils.doGet(url2, null, hashMap);
+                String substring = string2.substring(string2.indexOf("let data = {\"business_data\":") + "let data = {\"business_data\":".length(), string2.indexOf(",\"render_info\""));
+                JSONArray parsed = JSONObject.parseArray(substring);
+                JSONObject object = new JSONObject();
+                object.put("group_id", wordJson.getString("group_id"));
+                object.put("word", wordJson.getString("word"));
+                JSONArray array = new JSONArray();
+                for (int i1 = 0; i1 < parsed.size(); i1++) {
+                    String string1 = parsed.getJSONObject(i1).getJSONObject("data").getString("provider_doc_id_str");
+                    if (parsed.getJSONObject(i1).getInteger("type") == 1) {
+                        array.add("https://www.douyin.com/video/" + string1);
+                    }
+                }
+                object.put("list", array);
+                log.info("{}", object);
+            }
+
+        }
+
+    }
+
+    @Test
+    public void 获取直播视频流() {
+        String url = "https://live.douyin.com/660321581729";
+        HashMap<String, String> hashMap = new HashMap<>();
+        hashMap.put("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 Edg/148.0.0.0");
+        String done = OkHttpClientUtils.doGet(url, null, hashMap);
+        String roomId = DouYinUtils.getRegexString(done, "roomId[^\\d]*(\\d{15,})", 1);
+        if (StringUtils.isEmpty(roomId)) {
+            throw new RuntimeException("未找到房间号");
+        }
+        String url2 = "https://webcast.amemv.com/webcast/room/reflow/info/?type_id=0&live_id=1&room_id=" + roomId + "&sec_user_id=&app_id=1128";
+        String string = OkHttpClientUtils.doGet(url2, null, hashMap);
+        JSONObject data = JSONObject.parseObject(string);
+        JSONObject stream = data.getJSONObject("data").getJSONObject("room").getJSONObject("stream_url");
+        JSONObject resolutionName = stream.getJSONObject("resolution_name");
+        log.info("========================== flv ==========================");
+        JSONObject flv_pull_url = stream.getJSONObject("flv_pull_url");
+        flv_pull_url.forEach((key, value) -> {
+            String resolution = resolutionName.getString(key);
+            log.info("{}:{}", resolution, value);
+        });
+
+        log.info("========================== hls ==========================");
+        JSONObject hls_pull_url_map = stream.getJSONObject("hls_pull_url_map");
+        hls_pull_url_map.forEach((key, value) -> {
+            String resolution = resolutionName.getString(key);
+            log.info("{}:{}", resolution, value);
+        });
+
+        log.info("========================== 主播信息 ==========================");
+
+        JSONObject owner = data.getJSONObject("data").getJSONObject("room").getJSONObject("owner");
+        String avatar = owner.getJSONObject("avatar_large").getJSONArray("url_list").getString(0);
+        log.info("头像:{}", avatar);
+        log.info("昵称:{}", owner.getString("nickname"));
+        log.info("签名:{}", owner.getString("signature"));
+        Integer following_count = owner.getJSONObject("follow_info").getInteger("following_count");
+        log.info("关注数:{}", following_count);
+        Integer follower_count = owner.getJSONObject("follow_info").getInteger("follower_count");
+        log.info("粉丝数:{}", follower_count);
+        log.info("SECUID:{}", owner.getString("sec_uid"));
+        log.info("抖音号:{}", owner.getString("display_id"));
+
+
+    }
+
+    @Test
+    public void 获取抖音弹幕() {
+        String liveId = "561090451047";
+        try {
+
+            // 设置工作目录
+            String workDir = "C:\\Users\\Administrator\\Downloads\\DouyinLiveWebFetcher-main";
+
+            // 构建命令
+            String command = "cmd /c start cmd /k \"python main.py --live_id=" + liveId + "\"";
+
+            System.out.println("执行命令: " + command);
+
+            // 使用 ProcessBuilder 执行命令
+            ProcessBuilder processBuilder = new ProcessBuilder("cmd", "/c", command);
+            processBuilder.directory(new java.io.File(workDir));
+            // 合并错误流和输出流
+            processBuilder.redirectErrorStream(true);
+
+            // 启动进程
+            Process process = processBuilder.start();
+            // 等待进程自然结束
+            int exitCode = process.waitFor();
+            System.out.println("进程退出码: " + exitCode);
+
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}

+ 2 - 2
nexo-modules/nexo-system/src/main/java/com/nexo/system/controller/SysOperlogController.java

@@ -68,8 +68,8 @@ public class SysOperlogController extends BaseController {
     @SaCheckPermission("system:operlog:remove")
     @Log(title = "操作日志", businessType = BusinessType.CLEAN)
     @DeleteMapping("/clean")
-    public R<Void> clean() {
-        operLogService.cleanOperLog();
+    public R<Void> clean(Boolean isApi) {
+        operLogService.cleanOperLog(isApi);
         return R.ok();
     }
 

+ 1 - 1
nexo-modules/nexo-system/src/main/java/com/nexo/system/service/ISysOperLogService.java

@@ -50,5 +50,5 @@ public interface ISysOperLogService {
     /**
      * 清空操作日志
      */
-    void cleanOperLog();
+    void cleanOperLog(Boolean isApi);
 }

+ 7 - 2
nexo-modules/nexo-system/src/main/java/com/nexo/system/service/impl/SysOperLogServiceImpl.java

@@ -11,6 +11,7 @@ import com.nexo.system.mapper.SysOperLogMapper;
 import com.nexo.system.service.ISysOperLogService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.util.ObjectUtils;
 
 import java.util.Arrays;
 import java.util.Date;
@@ -34,6 +35,7 @@ public class SysOperLogServiceImpl implements ISysOperLogService {
         LambdaQueryWrapper<SysOperLog> lqw = new LambdaQueryWrapper<SysOperLog>()
             .like(StringUtils.isNotBlank(operLog.getOperIp()), SysOperLog::getOperIp, operLog.getOperIp())
             .like(StringUtils.isNotBlank(operLog.getTitle()), SysOperLog::getTitle, operLog.getTitle())
+            .eq(!ObjectUtils.isEmpty(operLog.getIsApi()), SysOperLog::getIsApi, operLog.getIsApi())
             .eq(operLog.getBusinessType() != null,
                 SysOperLog::getBusinessType, operLog.getBusinessType())
             .func(f -> {
@@ -78,6 +80,7 @@ public class SysOperLogServiceImpl implements ISysOperLogService {
         return baseMapper.selectList(new LambdaQueryWrapper<SysOperLog>()
             .like(StringUtils.isNotBlank(operLog.getOperIp()), SysOperLog::getOperIp, operLog.getOperIp())
             .like(StringUtils.isNotBlank(operLog.getTitle()), SysOperLog::getTitle, operLog.getTitle())
+            .eq(!ObjectUtils.isEmpty(operLog.getIsApi()), SysOperLog::getIsApi, operLog.getIsApi())
             .eq(operLog.getBusinessType() != null && operLog.getBusinessType() > 0,
                 SysOperLog::getBusinessType, operLog.getBusinessType())
             .func(f -> {
@@ -119,7 +122,9 @@ public class SysOperLogServiceImpl implements ISysOperLogService {
      * 清空操作日志
      */
     @Override
-    public void cleanOperLog() {
-        baseMapper.delete(new LambdaQueryWrapper<>());
+    public void cleanOperLog(Boolean isApi) {
+        LambdaQueryWrapper<SysOperLog> lambdaed = new LambdaQueryWrapper<>();
+        lambdaed.eq(SysOperLog::getIsApi, isApi);
+        baseMapper.delete(lambdaed);
     }
 }

+ 1 - 0
nexo-modules/nexo-system/src/main/resources/mapper/system/SysOperLogMapper.xml

@@ -20,6 +20,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 		<result property="status"         column="status"         />
 		<result property="errorMsg"       column="error_msg"      />
 		<result property="operTime"       column="oper_time"      />
+		<result property="isApi"          column="is_api"         />
 	</resultMap>
 
 </mapper>

+ 11 - 0
nexo-ui/src/api/douyin/douyinVideo.js

@@ -0,0 +1,11 @@
+import request from '@/utils/request'
+
+
+// 查询抖音用户信息列表
+export function parse(data) {
+  return request({
+    url: '/model/douyin/video/parse',
+    method: 'post',
+    data: data
+  })
+}

+ 5 - 2
nexo-ui/src/api/system/operlog.js

@@ -18,9 +18,12 @@ export function delOperlog(operId) {
 }
 
 // 清空操作日志
-export function cleanOperlog() {
+export function cleanOperlog(isApi) {
   return request({
     url: '/system/operlog/clean',
-    method: 'delete'
+    method: 'delete',
+    params: {
+      isApi: isApi
+    }
   })
 }

+ 130 - 0
nexo-ui/src/views/components/radio/nexo-radio.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="radio-inputs" :class="sizeClass">
+    <label v-for="(option, index) in options" :key="index" class="radio">
+      <input
+        type="radio"
+        :name="name"
+        :value="option.value"
+        :checked="option.value === currentValue"
+        @change="handleChange(option.value)"
+      >
+      <span class="name">{{ option.label }}</span>
+    </label>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'nexo-radio',
+  props: {
+    size: {
+      type: String,
+      default: 'small',
+      validator: (value) => {
+        return ['', 'medium', 'small', 'mini'].includes(value)
+      }
+    },
+    value: {
+      type: [String, Number],
+      default: ''
+    },
+    name: {
+      type: String,
+      default: 'radio'
+    },
+    options: {
+      type: Array,
+      default: () => [
+        { label: 'HTML', value: 'html' },
+        { label: 'React', value: 'react' },
+        { label: 'Vue', value: 'vue' }
+      ]
+    }
+  },
+  computed: {
+    currentValue() {
+      return this.value
+    },
+    sizeClass() {
+      if (this.size) {
+        return `nexo-radio--${this.size}`
+      }
+      return ''
+    }
+  },
+  methods: {
+    handleChange(value) {
+      this.$emit('input', value)
+      this.$emit('change', value)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.radio-inputs {
+  position: relative;
+  display: flex;
+  flex-wrap: wrap;
+  border-radius: 5px;
+  background-color: #fff;
+  box-sizing: border-box;
+  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06);
+  padding: 0.25rem;
+  width: 100%;
+  font-size: 14px;
+
+  &.nexo-radio--medium {
+    font-size: 14px;
+    padding: 0.3rem;
+
+    .name {
+      padding: 0.5rem 0;
+    }
+  }
+
+  &.nexo-radio--small {
+    font-size: 13px;
+    padding: 0.2rem;
+
+    .name {
+      padding: 0.4rem 0;
+    }
+  }
+
+  &.nexo-radio--mini {
+    font-size: 12px;
+    padding: 0.15rem;
+
+    .name {
+      padding: 0.3rem 0;
+    }
+  }
+}
+
+.radio-inputs .radio {
+  flex: 1 1 auto;
+  text-align: center;
+}
+
+.radio-inputs .radio input {
+  display: none;
+}
+
+.radio-inputs .radio .name {
+  display: flex;
+  cursor: pointer;
+  align-items: center;
+  justify-content: center;
+  border-radius: 5px;
+  border: none;
+  color: rgba(51, 65, 85, 1);
+  transition: all .15s ease-in-out;
+}
+
+.radio-inputs .radio input:checked + .name {
+  background-color: #0138c620;
+  color: #0138c6;
+  font-weight: 600;
+}
+</style>

+ 8 - 0
nexo-ui/src/views/components/video/svg/fanhui.svg

@@ -0,0 +1,8 @@
+<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="25"
+                 height="25"
+            >
+              <path
+                d="M679.49 141.56c12.8 0 25.59 4.88 35.36 14.64 19.53 19.53 19.53 51.18 0 70.71L431.26 510.49l283.58 283.58c19.53 19.53 19.53 51.18 0 70.71-19.53 19.53-51.19 19.52-70.71 0L325.2 545.84c-19.53-19.53-19.53-51.18 0-70.71L644.13 156.2c9.77-9.76 22.56-14.64 35.36-14.64z"
+                fill="#ffffff"
+              ></path>
+            </svg>

+ 16 - 0
nexo-ui/src/views/components/video/svg/fenxiang.svg

@@ -0,0 +1,16 @@
+<svg t="1779307817128" class="icon" viewBox="0 0 1024 1024" version="1.1"
+                 xmlns="http://www.w3.org/2000/svg" p-id="5966" width="22" height="22"
+            >
+              <path
+                d="M97.95349285 509.99901215m-94.17807172 0a94.17807171 94.17807171 0 1 0 188.35614346 0 94.17807171 94.17807171 0 1 0-188.35614346 0Z"
+                fill="#ffffff" p-id="5967"
+              ></path>
+              <path
+                d="M510.81520458 509.99901215m-94.17807172 0a94.17807171 94.17807171 0 1 0 188.35614344 0 94.17807171 94.17807171 0 1 0-188.35614344 0Z"
+                fill="#ffffff" p-id="5968"
+              ></path>
+              <path
+                d="M923.6769163 509.99901215m-94.17807172 0a94.17807171 94.17807171 0 1 0 188.35614343 0 94.17807171 94.17807171 0 1 0-188.35614343 0Z"
+                fill="#ffffff" p-id="5969"
+              ></path>
+            </svg>

+ 1 - 0
nexo-ui/src/views/components/video/svg/jingyin.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="1779962948709" class="icon" viewBox="0 0 1149 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="39051" xmlns:xlink="http://www.w3.org/1999/xlink" width="224.4140625" height="200"><path d="M607.969418 32.243307A124.014675 124.014675 0 0 0 432.792651 40.685561l-194.120981 213.599193a41.270416 41.270416 0 0 1-30.51417 13.527949h-63.571189A144.662597 144.662597 0 0 0 0 412.271872v199.359247a144.662597 144.662597 0 0 0 144.662597 144.662596h63.571188a41.270416 41.270416 0 0 1 30.514171 13.527949l194.120981 213.599194a124.014675 124.014675 0 0 0 215.760613-83.430828V124.014675a124.116388 124.116388 0 0 0-40.660132-91.771368z m-62.757477 867.645009a20.67335 20.67335 0 0 1-35.955864 13.909376l-194.120981-213.599193a144.713453 144.713453 0 0 0-107.028453-47.347822h-63.571189a41.321273 41.321273 0 0 1-41.321272-41.321272v-199.359247a41.321273 41.321273 0 0 1 41.321272-41.321273h63.571189a144.61174 144.61174 0 0 0 107.028453-47.347821l194.120981-213.599193a20.67335 20.67335 0 0 1 35.955864 13.909376z" p-id="39052" fill="#ffffff"></path><path d="M1167.420266 846.122651m-45.496969 24.385038l-0.022412 0.012012q-45.496969 24.385038-69.882007-21.111931l-335.540526-626.042777q-24.385038-45.496969 21.111931-69.882007l0.022413-0.012013q45.496969-24.385038 69.882007 21.111932l335.540525 626.042777q24.385038 45.496969-21.111931 69.882007Z" p-id="39053" fill="#ffffff"></path><path d="M789.875355 897.322791m-45.915617-23.587293l-0.022618-0.011619q-45.915617-23.587292-22.328325-69.50291l324.563468-631.803417q23.587292-45.915617 69.50291-22.328325l0.022618 0.011619q45.915617 23.587292 22.328325 69.50291l-324.563468 631.803417q-23.587292 45.915617-69.50291 22.328325Z" p-id="39054" fill="#ffffff"></path></svg>

+ 2 - 0
nexo-ui/src/views/components/video/svg/pause.svg

@@ -0,0 +1,2 @@
+<svg data-v-17ba6db7="" t="1769990158445" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1289" width="25" height="25"><path data-v-17ba6db7="" d="M332.26 853.89c-49.71 0-90-40.29-90-90v-503c0-49.71 40.29-90 90-90s90 40.29 90 90v503c0 49.7-40.3 90-90 90zM691.26 853.89c-49.71 0-90-40.29-90-90v-503c0-49.71 40.29-90 90-90s90 40.29 90 90v503c0 49.7-40.3 90-90 90z" p-id="1290" fill="#ffffff"></path></svg>
+          

+ 8 - 0
nexo-ui/src/views/components/video/svg/play.svg

@@ -0,0 +1,8 @@
+<svg class="icon" viewBox="0 0 1024 1024" version="1.1"
+     xmlns="http://www.w3.org/2000/svg" p-id="1068" width="25" height="25"
+>
+    <path
+            d="M783.74 401.86L372.23 155.28c-85.88-51.46-195.08 10.41-195.08 110.53v493.16c0 100.12 109.2 161.99 195.08 110.53l411.51-246.58c83.5-50.04 83.5-171.03 0-221.06z"
+            p-id="1069" fill="#ffffff"
+    ></path>
+</svg>

+ 8 - 0
nexo-ui/src/views/components/video/svg/quanping.svg

@@ -0,0 +1,8 @@
+<svg t="1769990261485" class="icon" viewBox="0 0 1024 1024" version="1.1"
+                 xmlns="http://www.w3.org/2000/svg" p-id="1510" width="25" height="25"
+            >
+              <path
+                d="M186.24 470.69c-30.38 0-55-24.62-55-55V304.61c0-92.96 75.63-168.6 168.6-168.6h111.08c30.38 0 55 24.62 55 55s-24.62 55-55 55H299.83c-32.31 0-58.6 26.29-58.6 58.6v111.08c0.01 30.38-24.62 55-54.99 55zM837.27 470.69c-30.38 0-55-24.62-55-55V304.61c0-32.31-26.29-58.6-58.6-58.6H612.6c-30.38 0-55-24.62-55-55s24.62-55 55-55h111.08c92.96 0 168.6 75.63 168.6 168.6v111.08c-0.01 30.38-24.63 55-55.01 55zM723.68 888.76H612.6c-30.38 0-55-24.62-55-55s24.62-55 55-55h111.08c32.31 0 58.6-26.29 58.6-58.6V609.08c0-30.38 24.62-55 55-55s55 24.62 55 55v111.08c-0.01 92.97-75.64 168.6-168.6 168.6zM410.91 888.76H299.83c-92.96 0-168.6-75.63-168.6-168.6V609.08c0-30.38 24.62-55 55-55s55 24.62 55 55v111.08c0 32.31 26.29 58.6 58.6 58.6h111.08c30.38 0 55 24.62 55 55s-24.62 55-55 55z"
+                p-id="1511" fill="#ffffff"
+              ></path>
+            </svg>

File diff suppressed because it is too large
+ 0 - 0
nexo-ui/src/views/components/video/svg/qxquanping.svg


+ 1 - 0
nexo-ui/src/views/components/video/svg/xiaochuang.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="1779962202974" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="17431" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M894.976 64c70.656 0 128 57.344 128 128v639.488c0 70.656-57.344 128-128 128H128c-70.656 0-128-57.344-128-128V192c0-70.656 57.344-128 128-128h766.976z m0 95.744H128c-15.36 0-28.672 11.264-31.232 26.112l-0.512 5.632v639.488c0 15.36 11.264 28.672 26.112 31.232l5.632 0.512h767.488c15.36 0 28.672-11.264 31.232-26.112l0.512-5.632V192c0-15.36-11.264-28.672-26.112-31.232l-6.144-1.024z m-127.488 287.744c35.328 0 64 28.672 64 64v192c0 35.328-28.672 64-64 64H468.992c-35.328 0-64-28.672-64-64V511.488c0-35.328 28.672-64 64-64h298.496z m0 0" p-id="17432" fill="#ffffff"></path></svg>

+ 1 - 0
nexo-ui/src/views/components/video/svg/yinliang.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="1779962767054" class="icon" viewBox="0 0 1156 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="33462" xmlns:xlink="http://www.w3.org/1999/xlink" width="225.78125" height="200"><path d="M315.164953 700.366562l194.139373 213.619431a20.675309 20.675309 0 0 0 35.959271-13.910694V124.026424a20.675309 20.675309 0 0 0-35.959271-13.910694l-194.139373 213.619431a144.625441 144.625441 0 0 1-107.038593 47.352307h-63.577212a41.325188 41.325188 0 0 0-41.325187 41.325188v199.378135a41.325188 41.325188 0 0 0 41.325187 41.325188h63.577212a144.727164 144.727164 0 0 1 107.038593 47.352307z m-170.386927 55.947947A144.676303 144.676303 0 0 1 0 611.689068v-199.378136a144.676303 144.676303 0 0 1 144.676303-144.676302h63.577211a41.274326 41.274326 0 0 0 30.517062-13.529231L432.833656 40.689415a124.026424 124.026424 0 0 1 215.781056 83.438733v775.845428a124.026424 124.026424 0 0 1-215.781056 83.438732l-194.139373-213.619431a41.274326 41.274326 0 0 0-30.517062-13.52923z m589.055581-449.923211a51.675558 51.675558 0 0 1 56.685441-86.465008 348.860875 348.860875 0 0 1 0 583.613371 51.675558 51.675558 0 0 1-56.685441-86.465007 245.484329 245.484329 0 0 0 0-410.78508z m120.059206-177.405851a51.675558 51.675558 0 1 1 59.457408-84.532261 571.101376 571.101376 0 0 1 0 934.661303 51.675558 51.675558 0 1 1-59.457408-84.532261 467.750261 467.750261 0 0 0 0-765.469627z" p-id="33463" fill="#ffffff"></path></svg>

+ 575 - 0
nexo-ui/src/views/components/video/video_play.vue

@@ -0,0 +1,575 @@
+<template>
+  <div class="video-container" @mousemove="handleMouseMove" @mouseleave="handleMouseLeave">
+    <div class="video-background" :style="{ backgroundImage: `url(${poster})` }"></div>
+    <video
+      ref="videoPlayer"
+      class="video-player"
+      controls="controls"
+      :src="video_url"
+      :poster="poster"
+      :autoplay="autoplay"
+      :controls="false"
+      :loop="loop"
+      width="100%"
+      height="100%"
+      @timeupdate="onTimeUpdate"
+      @loadedmetadata="onLoadedMetadata"
+      @progress="onProgress"
+      @ended="onEnded"
+      @play="onPlay"
+      @pause="onPause"
+      @leavepictureinpicture="onLeavePiP"
+    >
+      您的浏览器不支持 video 标签。
+    </video>
+    <div class="custom-controls" :class="{ 'controls-hidden': !showControls }">
+      <div class="mini-top-bar">
+        <div class="mini-top-left">
+          <div class="mini-top-icon" title="返回" @click="handleBack">
+            <img src="./svg/fanhui.svg" alt="返回"/>
+          </div>
+        </div>
+        <div class="mini-top-right">
+          <div class="mini-top-icon" title="分享" @click="handleShare">
+            <img src="./svg/fenxiang.svg" alt="分享"/>
+          </div>
+        </div>
+      </div>
+      <div class="center-play-btn" v-if="!isPlaying" @click="togglePlay">
+        <img src="./svg/play.svg" alt="播放"/>
+      </div>
+      <div class="video-click-area" @click="handleVideoClick"></div>
+      <div class="mini-controls">
+        <div class="mini-control-bar">
+          <div class="play-btn" @click="togglePlay">
+            <img v-if="!isPlaying" src="./svg/play.svg" alt="播放"/>
+            <img v-else src="./svg/pause.svg" alt="暂停"/>
+          </div>
+          <div class="time-current">{{ formatTime(currentTime) }}</div>
+          <div
+            class="mini-progress-bar"
+            ref="progressBar"
+            @click="onProgressBarClick"
+            @mousedown="onProgressBarMouseDown"
+          >
+            <div class="mini-progress-bg"></div>
+            <div class="mini-progress-buffered" :style="{ width: bufferedPercent + '%' }"></div>
+            <div class="mini-progress-played" :style="{ width: progressPercent + '%' }"></div>
+            <div class="mini-progress-dot" :style="{ left: progressPercent + '%' }"></div>
+          </div>
+          <div class="time-duration">{{ formatTime(duration) }}</div>
+          <div class="pip-btn" @click="togglePiP">
+            <img src="./svg/xiaochuang.svg" alt="小窗播放"/>
+          </div>
+          <div class="pip-btn" @click="toggleMute">
+            <img v-if="!isMuted" src="./svg/yinliang.svg" alt="音量"/>
+            <img v-else src="./svg/jingyin.svg" alt="静音"/>
+          </div>
+          <div class="fullscreen-btn" @click="toggleFullscreen">
+            <img src="./svg/quanping.svg" alt="全屏"/>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'video_play',
+  props: {
+    loop: {
+      type: Boolean,
+      default: false
+    },
+    controls: {
+      type: Boolean,
+      default: true
+    },
+    autoplay: {
+      type: Boolean,
+      default: false
+    },
+    poster: {
+      type: String,
+      default: ''
+    },
+    video_url: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      isMuted: false,
+      isPlaying: false,
+      currentTime: 0,
+      duration: 0,
+      buffered: 0,
+      showControls: true,
+      controlsTimer: null,
+      isDragging: false,
+      isPiPActive: false
+    }
+  },
+  computed: {
+    progressPercent() {
+      if (this.duration === 0) return 0
+      return (this.currentTime / this.duration) * 100
+    },
+    bufferedPercent() {
+      if (this.duration === 0) return 0
+      return (this.buffered / this.duration) * 100
+    },
+    isPiPSupported() {
+      const video = this.$refs.videoPlayer
+      return video && (video.requestPictureInPicture || document.pictureInPictureEnabled)
+    }
+  },
+  mounted() {
+  },
+  beforeDestroy() {
+    if (this.isPiPActive) {
+      this.exitPiP()
+    }
+  },
+  methods: {
+    togglePlay() {
+      const video = this.$refs.videoPlayer
+      if (video.paused) {
+        video.play()
+      } else {
+        video.pause()
+      }
+    },
+    handleVideoClick(e) {
+      if (e.target.closest('.mini-top-bar') ||
+        e.target.closest('.mini-control-bar') ||
+        e.target.closest('.center-play-btn')) {
+        return
+      }
+      this.togglePlay()
+    },
+    async togglePiP() {
+      const video = this.$refs.videoPlayer
+
+      try {
+        if (document.pictureInPictureElement) {
+          await this.exitPiP()
+        } else {
+          await this.enterPiP(video)
+        }
+      } catch (error) {
+        console.error('画中画切换失败:', error)
+        this.$message && this.$message.warning('画中画功能不可用')
+      }
+    },
+    async enterPiP(video) {
+      if (video.requestPictureInPicture) {
+        await video.requestPictureInPicture()
+        this.isPiPActive = true
+        this.$emit('pip-enter')
+      }
+    },
+    async exitPiP() {
+      if (document.pictureInPictureElement) {
+        await document.exitPictureInPicture()
+        this.isPiPActive = false
+        this.$emit('pip-exit')
+      }
+    },
+    onLeavePiP() {
+      this.isPiPActive = false
+      this.$emit('pip-exit')
+    },
+    toggleMute() {
+      const video = this.$refs.videoPlayer
+      if (this.isMuted) {
+        video.muted = false
+        this.isMuted = false
+        video.volume = this.previousVolume
+        this.volume = this.previousVolume
+      } else {
+        this.previousVolume = this.volume
+        video.muted = true
+        this.isMuted = true
+      }
+    },
+    onPlay() {
+      this.isPlaying = true
+    },
+    onPause() {
+      this.isPlaying = false
+    },
+    onTimeUpdate() {
+      const video = this.$refs.videoPlayer
+      this.currentTime = video.currentTime
+    },
+    onLoadedMetadata() {
+      const video = this.$refs.videoPlayer
+      this.duration = video.duration
+    },
+    onProgress() {
+      const video = this.$refs.videoPlayer
+      if (video.buffered.length > 0) {
+        this.buffered = video.buffered.end(video.buffered.length - 1)
+      }
+    },
+    onEnded() {
+      this.isPlaying = false
+      this.currentTime = 0
+      if (this.isPiPActive) {
+        this.exitPiP()
+      }
+    },
+    formatTime(seconds) {
+      if (!seconds || isNaN(seconds)) return '00:00'
+      const mins = Math.floor(seconds / 60)
+      const secs = Math.floor(seconds % 60)
+      return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
+    },
+    onProgressBarClick(e) {
+      const progressBar = this.$refs.progressBar
+      const rect = progressBar.getBoundingClientRect()
+      const pos = (e.clientX - rect.left) / rect.width
+      const video = this.$refs.videoPlayer
+      video.currentTime = pos * this.duration
+    },
+    onProgressBarMouseDown(e) {
+      this.isDragging = true
+      const handleMouseMove = (e) => {
+        if (!this.isDragging) return
+        const progressBar = this.$refs.progressBar
+        const rect = progressBar.getBoundingClientRect()
+        let pos = (e.clientX - rect.left) / rect.width
+        pos = Math.max(0, Math.min(1, pos))
+        const video = this.$refs.videoPlayer
+        video.currentTime = pos * this.duration
+      }
+      const handleMouseUp = () => {
+        this.isDragging = false
+        document.removeEventListener('mousemove', handleMouseMove)
+        document.removeEventListener('mouseup', handleMouseUp)
+      }
+      document.addEventListener('mousemove', handleMouseMove)
+      document.addEventListener('mouseup', handleMouseUp)
+    },
+    toggleFullscreen() {
+      const container = this.$el
+      if (!document.fullscreenElement) {
+        if (container.requestFullscreen) {
+          container.requestFullscreen()
+        } else if (container.webkitRequestFullscreen) {
+          container.webkitRequestFullscreen()
+        } else if (container.mozRequestFullScreen) {
+          container.mozRequestFullScreen()
+        } else if (container.msRequestFullscreen) {
+          container.msRequestFullscreen()
+        }
+      } else {
+        if (document.exitFullscreen) {
+          document.exitFullscreen()
+        } else if (document.webkitExitFullscreen) {
+          document.webkitExitFullscreen()
+        } else if (document.mozCancelFullScreen) {
+          document.mozCancelFullScreen()
+        } else if (document.msExitFullscreen) {
+          document.msExitFullscreen()
+        }
+      }
+    },
+    handleBack() {
+      if (this.isPiPActive) {
+        this.exitPiP()
+      }
+      this.$emit('back')
+      if (this.$router && this.$router.back) {
+        this.$router.back()
+      }
+    },
+    handleShare() {
+      this.$emit('share', {
+        url: this.video_url,
+        poster: this.poster
+      })
+    },
+    handleMouseMove() {
+      this.showControls = true
+      if (this.controlsTimer) {
+        clearTimeout(this.controlsTimer)
+      }
+      if (this.isPlaying) {
+        this.controlsTimer = setTimeout(() => {
+          this.showControls = false
+        }, 3000)
+      }
+    },
+    handleMouseLeave() {
+      if (this.isPlaying) {
+        this.showControls = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+
+.video-container {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  border-radius: 8px;
+}
+
+.video-background {
+  position: absolute;
+  top: -20px;
+  left: -20px;
+  right: -20px;
+  bottom: -20px;
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+  filter: blur(20px);
+  transform: scale(1.1);
+  z-index: 0;
+}
+
+.video-player {
+  position: relative;
+  z-index: 1;
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+}
+
+.custom-controls {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none;
+  z-index: 35;
+  transition: opacity .3s, visibility .3s;
+  visibility: visible;
+  opacity: 1;
+
+  &.controls-hidden {
+    opacity: 0;
+    visibility: hidden;
+
+    .mini-controls {
+      pointer-events: none;
+    }
+  }
+}
+
+
+.custom-controls .mini-top-bar {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  padding: 8px 12px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background: linear-gradient(to bottom, rgba(0, 0, 0, .55), transparent);
+  z-index: 25;
+  pointer-events: auto
+}
+
+.custom-controls .mini-top-left {
+  margin-left: -4px
+}
+
+.custom-controls .mini-top-left, .custom-controls .mini-top-right {
+  display: flex;
+  align-items: center;
+  gap: 12px
+}
+
+.custom-controls .mini-top-icon {
+  width: 32px;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-radius: 50%;
+  cursor: pointer;
+  transition: background .2s
+}
+
+.custom-controls .mini-top-icon img {
+  width: 20px;
+  height: 20px;
+  object-fit: contain;
+}
+
+@media(hover: hover) {
+  .custom-controls .mini-top-icon:hover {
+    background: rgba(255, 255, 255, .15)
+  }
+}
+
+.center-play-btn {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  cursor: pointer;
+  z-index: 25;
+  pointer-events: auto;
+
+  img {
+    width: 60px;
+    height: 60px;
+    object-fit: contain;
+  }
+}
+
+.video-click-area {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: calc(100% - 60px);
+  z-index: 20;
+  pointer-events: auto;
+  cursor: pointer;
+}
+
+
+.mini-controls {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  pointer-events: none
+}
+
+.mini-controls .center-play-icon {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  pointer-events: auto;
+  z-index: 20
+}
+
+.mini-controls .mini-control-bar {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  padding: 8px 12px;
+  background: linear-gradient(to top, rgba(0, 0, 0, .8), transparent);
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  pointer-events: auto;
+  z-index: 15;
+  transition: transform .3s ease, opacity .3s ease;
+  transform: translateY(0)
+}
+
+@media only screen and (min-width: 769px) {
+  .mini-controls .mini-control-bar {
+    padding: 16px 12px
+  }
+}
+
+.controls-hidden .mini-controls .mini-control-bar {
+  transform: translateY(100%);
+  opacity: 0
+}
+
+.mini-controls .mini-control-bar .play-btn,
+.mini-controls .mini-control-bar .pip-btn,
+.mini-controls .mini-control-bar .fullscreen-btn {
+  width: 32px;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  border-radius: 50%;
+  transition: background .2s;
+  flex-shrink: 0
+}
+
+.mini-controls .mini-control-bar .play-btn img,
+.mini-controls .mini-control-bar .pip-btn img,
+.mini-controls .mini-control-bar .fullscreen-btn img {
+  width: 20px;
+  height: 20px;
+  object-fit: contain;
+}
+
+@media(hover: hover) {
+  .mini-controls .mini-control-bar .play-btn:hover,
+  .mini-controls .mini-control-bar .pip-btn:hover,
+  .mini-controls .mini-control-bar .fullscreen-btn:hover {
+    background: rgba(255, 255, 255, .15)
+  }
+}
+
+.mini-controls .mini-control-bar .time-current, .mini-controls .mini-control-bar .time-duration {
+  color: #fff;
+  font-size: 14px;
+  min-width: 40px;
+  flex-shrink: 0;
+  text-align: center
+}
+
+.mini-controls .mini-control-bar .time-current {
+  margin-right: 4px
+}
+
+.mini-controls .mini-control-bar .time-duration {
+  margin-left: 4px
+}
+
+.mini-controls .mini-control-bar .mini-progress-bar {
+  flex: 1;
+  height: 20px;
+  display: flex;
+  align-items: center;
+  position: relative;
+  cursor: pointer
+}
+
+.mini-controls .mini-control-bar .mini-progress-bar .mini-progress-bg, .mini-controls .mini-control-bar .mini-progress-bar .mini-progress-buffered, .mini-controls .mini-control-bar .mini-progress-bar .mini-progress-played {
+  position: absolute;
+  height: 4px;
+  border-radius: 2px
+}
+
+.mini-controls .mini-control-bar .mini-progress-bar .mini-progress-bg {
+  width: 100%;
+  background: rgba(255, 255, 255, .3)
+}
+
+.mini-controls .mini-control-bar .mini-progress-bar .mini-progress-buffered {
+  background: rgba(255, 255, 255, .5)
+}
+
+.mini-controls .mini-control-bar .mini-progress-bar .mini-progress-played {
+  background: #fff
+}
+
+.mini-controls .mini-control-bar .mini-progress-bar .mini-progress-dot {
+  position: absolute;
+  width: 10px;
+  height: 10px;
+  background: #fff;
+  border-radius: 50%;
+  transform: translate(-50%);
+  box-shadow: 0 0 4px rgba(0, 0, 0, .3)
+}
+
+</style>

+ 128 - 0
nexo-ui/src/views/module/douyin/worksAanalysis/index.vue

@@ -0,0 +1,128 @@
+<template>
+  <div class="app-container">
+    <el-row :gutter="15">
+      <el-col :xs="24" :sm="12" :md="12" :lg="16" :xl="16">
+        <nexo-radio v-model="type" :options="radioOptions" style="width: 350px"/>
+        <el-card shadow="never" class="mt10">
+          <el-form label-width="80px" ref="form" :model="form" :rules="rules">
+            <el-form-item label="视频地址" v-if="type === 0" prop="videoUrl">
+              <el-input v-model="form.videoUrl" placeholder="请输入视频地址"/>
+            </el-form-item>
+            <el-form-item label="视频地址" v-if="type === 1" prop="videoUrl">
+              <el-input type="textarea" v-model="form.videoUrl" placeholder="请输入视频地址" :rows="10"/>
+              <span style="font-size: 12px;font-weight: 600;color: #F56C6C">
+                * 注: 请将视频地址填写到每一行
+              </span>
+            </el-form-item>
+            <el-form-item class="submit-but">
+              <el-button type="primary" icon="el-icon-s-promotion" size="mini" @click="submitForm">提交解析</el-button>
+              <el-button icon="el-icon-refresh" size="mini" @click="reset">重置地址</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+        <el-card shadow="never" class="mt10">
+          <div slot="header" class="clearfix">
+            <span>作品信息</span>
+          </div>
+          <div class="video-info">
+            <div><span v-if="data.avatar">作者avatar: <el-avatar :src="data.avatar" :size="50" shape="square"/></span>
+            </div>
+            <div><span v-if="data.sec_uid">作者SecUid: {{ data.sec_uid }}</span></div>
+            <div><span v-if="data.short_id">作者shortId: {{ data.short_id }}</span></div>
+            <div><span v-if="data.unique_id">作者抖音号: {{ data.unique_id }}</span></div>
+            <div><span v-if="data.nickname">作者昵称: {{ data.nickname }}</span></div>
+            <div><span v-if="data.signature">作者签名: {{ data.signature }}</span></div>
+            <div><span v-if="data.desc">作品标题: {{ data.desc }}</span></div>
+            <div><span v-if="data.comment_count">comment_count: {{ data.comment_count }}</span></div>
+            <div><span v-if="data.share_count">share_count: {{ data.share_count }}</span></div>
+            <div><span v-if="data.digg_count">digg_count: {{ data.digg_count }}</span></div>
+            <div><span v-if="data.collect_count">collect_count: {{ data.collect_count }}</span></div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8">
+        <el-card shadow="never">
+          <div slot="header" class="clearfix">
+            <span>视频预览</span>
+          </div>
+          <video_play style="height: 80vh" :video_url="data.play_addr" :poster="data.cover"></video_play>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+<script>
+import Video_play from '@/views/components/video/video_play.vue'
+import NexoRadio from '@/views/components/radio/nexo-radio.vue'
+import { parse } from '@/api/douyin/douyinVideo'
+
+export default {
+  name: 'index',
+  components: { NexoRadio, Video_play },
+  data() {
+    return {
+      type: 0,
+      radioOptions: [
+        { label: '孤品 · 深度对谈', value: 0 },
+        { label: '万象 · 全局洞察', value: 1 }
+      ],
+      form: {},
+      rules: {
+        videoUrl: [
+          { required: true, message: '请输入需要解析的视频地址', trigger: 'blur' }
+        ]
+      },
+      data: {}
+    }
+  },
+  watch: {
+    type(val) {
+      this.reset()
+      this.form.type = val
+    }
+  },
+  methods: {
+    submitForm() {
+      console.log('submitForm', this.form)
+      this.form.type = this.type
+      this.$refs['form'].validate((valid) => {
+        if (valid) {
+          parse(this.form).then(response => {
+            this.data = response.data
+          })
+        } else {
+          console.log('error submit!!')
+          return false
+        }
+      })
+    },
+    reset() {
+      this.form = {
+        type: 0,
+        videoUrl: 'https://www.douyin.com/jingxuan?modal_id=7643814124878656421'
+      }
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+::v-deep .el-card__header {
+  padding: 10px 20px;
+}
+
+.submit-but {
+  margin-bottom: 0 !important;
+}
+
+.video-info {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  font-size: 14px;
+  font-weight: 600;
+  div{
+    border-bottom:  1px solid #eee;
+  }
+}
+</style>

+ 3 - 1
nexo-ui/src/views/system/operlog/index.vue → nexo-ui/src/views/monitor/interfacelog/index.vue

@@ -232,6 +232,7 @@ export default {
     /** 查询登录日志 */
     getList() {
       this.loading = true;
+      this.queryParams['isApi'] = true
       list(this.addDateRange(this.queryParams, this.dateRange)).then( response => {
           this.list = response.rows;
           this.total = response.total;
@@ -284,7 +285,7 @@ export default {
     /** 清空按钮操作 */
     handleClean() {
       this.$modal.confirm('是否确认清空所有操作日志数据项?').then(function() {
-        return cleanOperlog();
+        return cleanOperlog(true);
       }).then(() => {
         this.getList();
         this.$modal.msgSuccess("清空成功");
@@ -292,6 +293,7 @@ export default {
     },
     /** 导出按钮操作 */
     handleExport() {
+      this.queryParams.isApi = true
       this.download('system/operlog/export', {
         ...this.queryParams
       }, `operlog_${new Date().getTime()}.xlsx`)

+ 0 - 0
nexo-ui/src/views/system/logininfor/index.vue → nexo-ui/src/views/monitor/logininfor/index.vue


+ 304 - 0
nexo-ui/src/views/monitor/operlog/index.vue

@@ -0,0 +1,304 @@
+<template>
+  <div class="app-container">
+    <el-card shadow="never">
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="系统模块" prop="title">
+        <el-input
+          v-model="queryParams.title"
+          placeholder="请输入系统模块"
+          clearable
+          style="width: 240px;"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="类型" prop="businessType">
+        <el-select
+          v-model="queryParams.businessType"
+          placeholder="操作类型"
+          clearable
+          style="width: 240px"
+        >
+          <el-option
+            v-for="dict in dict.type.sys_oper_type"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="操作状态"
+          clearable
+          style="width: 240px"
+        >
+          <el-option
+            v-for="dict in dict.type.sys_common_status"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="操作时间">
+        <el-date-picker
+          v-model="dateRange"
+          style="width: 240px"
+          value-format="yyyy-MM-dd HH:mm:ss"
+          type="daterange"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="['00:00:00', '23:59:59']"
+        ></el-date-picker>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+    </el-card>
+    <el-card shadow="never" class="mt10">
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['system:operlog:remove']"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          @click="handleClean"
+          v-hasPermi="['system:operlog:remove']"
+        >清空</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExport"
+          v-hasPermi="['system:operlog:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table ref="tables" v-loading="loading" :data="list" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="日志编号" align="center" prop="operId" />
+      <el-table-column label="系统模块" align="center" prop="title" />
+      <el-table-column label="操作类型" align="center" prop="businessType">
+        <template slot-scope="scope">
+          <dict-tag :options="dict.type.sys_oper_type" :value="scope.row.businessType"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="请求方式" align="center" prop="requestMethod" />
+      <el-table-column label="操作人员" align="center" prop="operName" width="100" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
+      <el-table-column label="部门" align="center" prop="deptName" width="130" :show-overflow-tooltip="true" />
+      <el-table-column label="操作地址" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
+      <el-table-column label="操作地点" align="center" prop="operLocation" :show-overflow-tooltip="true" />
+      <el-table-column label="操作状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <dict-tag :options="dict.type.sys_common_status" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作日期" align="center" prop="operTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.operTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-view"
+            @click="handleView(scope.row,scope.index)"
+            v-hasPermi="['system:operlog:query']"
+          >详细</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+    </el-card>
+    <!-- 操作日志详细 -->
+    <el-dialog title="操作日志详细" :visible.sync="open" width="700px" append-to-body>
+      <el-form ref="form" :model="form" label-width="100px" size="mini">
+        <el-row>
+          <el-col :span="24">
+            <el-form-item
+              label="登录信息:"
+            >{{ form.operName }} / {{form.deptName}} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="请求信息:">{{ form.requestMethod }} {{form.operUrl }}</el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="操作模块:">{{ form.title }} / {{ typeFormat(form) }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="操作方法:">{{ form.method }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="请求参数:">{{ form.operParam }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="返回参数:">{{ form.jsonResult }}</el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="操作状态:">
+              <div v-if="form.status === 0">正常</div>
+              <div v-else-if="form.status === 1">失败</div>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="操作时间:">{{ parseTime(form.operTime) }}</el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="异常信息:" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="open = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { list, delOperlog, cleanOperlog } from "@/api/system/operlog";
+
+export default {
+  name: "Operlog",
+  dicts: ['sys_oper_type', 'sys_common_status'],
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 表格数据
+      list: [],
+      // 是否显示弹出层
+      open: false,
+      // 日期范围
+      dateRange: [],
+      // 默认排序
+      defaultSort: {prop: 'operTime', order: 'descending'},
+      // 表单参数
+      form: {},
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        operIp: undefined,
+        title: undefined,
+        operName: undefined,
+        businessType: undefined,
+        status: undefined
+      }
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询登录日志 */
+    getList() {
+      this.loading = true;
+      this.queryParams['isApi'] = false
+      list(this.addDateRange(this.queryParams, this.dateRange)).then( response => {
+          this.list = response.rows;
+          this.total = response.total;
+          this.loading = false;
+        }
+      );
+    },
+    // 操作日志类型字典翻译
+    typeFormat(row, column) {
+      return this.selectDictLabel(this.dict.type.sys_oper_type, row.businessType);
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRange = [];
+      this.resetForm("queryForm");
+      this.queryParams.pageNum = 1;
+      this.$refs.tables.sort(this.defaultSort.prop, this.defaultSort.order)
+    },
+    /** 多选框选中数据 */
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.operId)
+      this.multiple = !selection.length
+    },
+    /** 排序触发事件 */
+    handleSortChange(column, prop, order) {
+      this.queryParams.orderByColumn = column.prop;
+      this.queryParams.isAsc = column.order;
+      this.getList();
+    },
+    /** 详细按钮操作 */
+    handleView(row) {
+      this.open = true;
+      this.form = row;
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const operIds = row.operId || this.ids;
+      this.$modal.confirm('是否确认删除日志编号为"' + operIds + '"的数据项?').then(function() {
+        return delOperlog(operIds);
+      }).then(() => {
+        this.getList();
+        this.$modal.msgSuccess("删除成功");
+      }).catch(() => {});
+    },
+    /** 清空按钮操作 */
+    handleClean() {
+      this.$modal.confirm('是否确认清空所有操作日志数据项?').then(function() {
+        return cleanOperlog(false);
+      }).then(() => {
+        this.getList();
+        this.$modal.msgSuccess("清空成功");
+      }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      this.queryParams.isApi = false
+      this.download('system/operlog/export', {
+        ...this.queryParams
+      }, `operlog_${new Date().getTime()}.xlsx`)
+    }
+  }
+};
+</script>
+

Some files were not shown because too many files changed in this diff