From 8bfe1168a42d4e3750a15b0c0fb0a7629d6cf91c Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期三, 01 四月 2026 17:46:53 +0800
Subject: [PATCH] #日志清理与手动操作权限

---
 src/main/webapp/views/config/config.html                                    |   58 ++
 src/main/java/com/zy/system/model/LogCleanupExecutionResult.java            |   20 +
 src/main/resources/sql/20260401_add_log_cleanup_menu_and_config.sql         |   80 ++++
 src/main/webapp/static/js/wrkMast/wrkMast.js                                |    4 
 src/main/java/com/zy/asrs/controller/WrkMastController.java                 |   25 +
 src/main/java/com/zy/system/service/impl/HighPrivilegeGrantServiceImpl.java |  114 +++++
 src/main/java/com/zy/system/domain/param/LogCleanupConfigParam.java         |    9 
 src/main/resources/mapper/LogCleanupMapper.xml                              |   11 
 src/main/java/com/zy/system/mapper/LogCleanupMapper.java                    |   15 
 src/main/java/com/zy/system/model/HighPrivilegeGrantStatus.java             |   13 
 src/main/java/com/zy/system/service/LogCleanupService.java                  |   20 +
 src/main/java/com/zy/system/controller/HighPrivilegeGrantController.java    |   33 +
 src/main/java/com/zy/system/controller/LogCleanupController.java            |   63 +++
 src/main/webapp/static/js/config/config.js                                  |  158 +++++++
 src/main/java/com/zy/core/enums/RedisKeyType.java                           |    1 
 src/main/java/com/zy/system/service/HighPrivilegeGrantService.java          |   13 
 src/main/java/com/zy/system/domain/param/HighPrivilegeGrantParam.java       |   11 
 src/main/java/com/zy/system/domain/param/LogCleanupRunParam.java            |   13 
 src/main/java/com/zy/system/timer/LogCleanupScheduler.java                  |   24 +
 src/main/java/com/zy/system/service/impl/LogCleanupServiceImpl.java         |  168 ++++++++
 src/main/webapp/static/js/logCleanup/logCleanup.js                          |  164 ++++++++
 src/main/webapp/views/logCleanup/logCleanup.html                            |  165 ++++++++
 22 files changed, 1,179 insertions(+), 3 deletions(-)

diff --git a/src/main/java/com/zy/asrs/controller/WrkMastController.java b/src/main/java/com/zy/asrs/controller/WrkMastController.java
index 51496f6..9ef23d0 100644
--- a/src/main/java/com/zy/asrs/controller/WrkMastController.java
+++ b/src/main/java/com/zy/asrs/controller/WrkMastController.java
@@ -7,10 +7,13 @@
 import com.core.common.DateUtils;
 import com.core.common.R;
 import com.zy.asrs.domain.param.CreateOutTaskBatchParam;
+import com.zy.asrs.domain.param.CancelTaskParam;
+import com.zy.asrs.domain.param.CompleteTaskParam;
 import com.zy.asrs.entity.WrkMast;
 import com.zy.asrs.service.WrkMastService;
 import com.zy.common.service.CommonService;
 import com.zy.common.web.BaseController;
+import com.zy.system.service.HighPrivilegeGrantService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
@@ -30,6 +33,8 @@
     private WrkMastService wrkMastService;
     @Autowired
     private CommonService commonService;
+    @Autowired
+    private HighPrivilegeGrantService highPrivilegeGrantService;
 
     @RequestMapping(value = "/wrkMast/list/auth")
     @ManagerAuth
@@ -68,6 +73,26 @@
         return R.error("鐢熸垚鎵归噺鍑哄簱浠诲姟澶辫触");
     }
 
+    @PostMapping(value = "/wrkMast/complete/auth")
+    @ManagerAuth(memo = "宸ヤ綔妗e畬鎴愪换鍔�")
+    public R completeTask(@RequestBody CompleteTaskParam param) {
+        if (param == null) {
+            return R.error("鍙傛暟涓嶈兘涓虹┖");
+        }
+        highPrivilegeGrantService.assertGranted(request.getHeader("token"), "瀹屾垚浠诲姟");
+        return commonService.completeTask(param) ? R.ok() : R.error("浠诲姟瀹屾垚澶辫触");
+    }
+
+    @PostMapping(value = "/wrkMast/cancel/auth")
+    @ManagerAuth(memo = "宸ヤ綔妗e彇娑堜换鍔�")
+    public R cancelTask(@RequestBody CancelTaskParam param) {
+        if (param == null) {
+            return R.error("鍙傛暟涓嶈兘涓虹┖");
+        }
+        highPrivilegeGrantService.assertGranted(request.getHeader("token"), "鍙栨秷浠诲姟");
+        return commonService.cancelTask(param) ? R.ok() : R.error("浠诲姟鍙栨秷澶辫触");
+    }
+
     private <T> void convert(Map<String, Object> map, QueryWrapper<T> wrapper){
         for (Map.Entry<String, Object> entry : map.entrySet()){
             String val = String.valueOf(entry.getValue());
diff --git a/src/main/java/com/zy/core/enums/RedisKeyType.java b/src/main/java/com/zy/core/enums/RedisKeyType.java
index f41324b..c4c9cfc 100644
--- a/src/main/java/com/zy/core/enums/RedisKeyType.java
+++ b/src/main/java/com/zy/core/enums/RedisKeyType.java
@@ -74,6 +74,7 @@
     CURRENT_CIRCLE_TASK_CRN_NO("current_circle_task_crn_no_"),
     MAIN_PROCESS_PSEUDOCODE("main_process_pseudocode"),
     PLANNER_SCHEDULE("planner_schedule_"),
+    HIGH_PRIVILEGE_GRANT("high_privilege_grant_"),
     ;
 
     public String key;
diff --git a/src/main/java/com/zy/system/controller/HighPrivilegeGrantController.java b/src/main/java/com/zy/system/controller/HighPrivilegeGrantController.java
new file mode 100644
index 0000000..5cd3815
--- /dev/null
+++ b/src/main/java/com/zy/system/controller/HighPrivilegeGrantController.java
@@ -0,0 +1,33 @@
+package com.zy.system.controller;
+
+import com.core.annotations.ManagerAuth;
+import com.core.common.R;
+import com.zy.common.web.BaseController;
+import com.zy.system.domain.param.HighPrivilegeGrantParam;
+import com.zy.system.model.HighPrivilegeGrantStatus;
+import com.zy.system.service.HighPrivilegeGrantService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class HighPrivilegeGrantController extends BaseController {
+
+    @Autowired
+    private HighPrivilegeGrantService highPrivilegeGrantService;
+
+    @PostMapping("/highPrivilege/grant/auth")
+    @ManagerAuth(memo = "鏈�楂樻潈闄愭巿鏉�")
+    public R grant(@RequestBody HighPrivilegeGrantParam param) {
+        HighPrivilegeGrantStatus status = highPrivilegeGrantService.grant(request.getHeader("token"), param);
+        return R.ok().add(status);
+    }
+
+    @GetMapping("/highPrivilege/status/auth")
+    @ManagerAuth
+    public R status() {
+        return R.ok().add(highPrivilegeGrantService.getStatus(request.getHeader("token")));
+    }
+}
diff --git a/src/main/java/com/zy/system/controller/LogCleanupController.java b/src/main/java/com/zy/system/controller/LogCleanupController.java
new file mode 100644
index 0000000..d6abaf4
--- /dev/null
+++ b/src/main/java/com/zy/system/controller/LogCleanupController.java
@@ -0,0 +1,63 @@
+package com.zy.system.controller;
+
+import com.core.annotations.ManagerAuth;
+import com.core.common.R;
+import com.zy.common.web.BaseController;
+import com.zy.system.domain.param.LogCleanupConfigParam;
+import com.zy.system.domain.param.LogCleanupRunParam;
+import com.zy.system.model.LogCleanupExecutionResult;
+import com.zy.system.service.HighPrivilegeGrantService;
+import com.zy.system.service.LogCleanupService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@RestController
+public class LogCleanupController extends BaseController {
+
+    @Autowired
+    private LogCleanupService logCleanupService;
+    @Autowired
+    private HighPrivilegeGrantService highPrivilegeGrantService;
+
+    @GetMapping("/logCleanup/config/auth")
+    @ManagerAuth
+    public R getConfig() {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("expireDays", logCleanupService.getExpireDays());
+        result.put("autoTime", "姣忔棩 23:00");
+        result.put("tables", logCleanupService.getSupportedTables());
+        return R.ok().add(result);
+    }
+
+    @PostMapping("/logCleanup/config/save/auth")
+    @ManagerAuth(memo = "淇濆瓨鏃ュ織娓呯悊閰嶇疆")
+    public R saveConfig(@RequestBody LogCleanupConfigParam param) {
+        if (param == null) {
+            return R.error("鍙傛暟涓嶈兘涓虹┖");
+        }
+        logCleanupService.saveExpireDays(param.getExpireDays());
+        return R.ok();
+    }
+
+    @PostMapping("/logCleanup/run/auth")
+    @ManagerAuth(memo = "鎵嬪姩娓呯悊鏃ュ織")
+    public R run(@RequestBody LogCleanupRunParam param) {
+        if (param == null) {
+            return R.error("鍙傛暟涓嶈兘涓虹┖");
+        }
+        highPrivilegeGrantService.assertGranted(request.getHeader("token"), "鎵嬪姩娓呯悊鏃ュ織");
+        LogCleanupExecutionResult result;
+        if ("selected".equals(param.getMode())) {
+            result = logCleanupService.cleanupSelected(logCleanupService.getExpireDays(), param.getTables());
+        } else {
+            result = logCleanupService.cleanupAll(logCleanupService.getExpireDays());
+        }
+        return R.ok().add(result);
+    }
+}
diff --git a/src/main/java/com/zy/system/domain/param/HighPrivilegeGrantParam.java b/src/main/java/com/zy/system/domain/param/HighPrivilegeGrantParam.java
new file mode 100644
index 0000000..5524849
--- /dev/null
+++ b/src/main/java/com/zy/system/domain/param/HighPrivilegeGrantParam.java
@@ -0,0 +1,11 @@
+package com.zy.system.domain.param;
+
+import lombok.Data;
+
+@Data
+public class HighPrivilegeGrantParam {
+
+    private String account;
+
+    private String password;
+}
diff --git a/src/main/java/com/zy/system/domain/param/LogCleanupConfigParam.java b/src/main/java/com/zy/system/domain/param/LogCleanupConfigParam.java
new file mode 100644
index 0000000..8d5f830
--- /dev/null
+++ b/src/main/java/com/zy/system/domain/param/LogCleanupConfigParam.java
@@ -0,0 +1,9 @@
+package com.zy.system.domain.param;
+
+import lombok.Data;
+
+@Data
+public class LogCleanupConfigParam {
+
+    private Integer expireDays;
+}
diff --git a/src/main/java/com/zy/system/domain/param/LogCleanupRunParam.java b/src/main/java/com/zy/system/domain/param/LogCleanupRunParam.java
new file mode 100644
index 0000000..53e7bfb
--- /dev/null
+++ b/src/main/java/com/zy/system/domain/param/LogCleanupRunParam.java
@@ -0,0 +1,13 @@
+package com.zy.system.domain.param;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class LogCleanupRunParam {
+
+    private String mode;
+
+    private List<String> tables;
+}
diff --git a/src/main/java/com/zy/system/mapper/LogCleanupMapper.java b/src/main/java/com/zy/system/mapper/LogCleanupMapper.java
new file mode 100644
index 0000000..3fdb494
--- /dev/null
+++ b/src/main/java/com/zy/system/mapper/LogCleanupMapper.java
@@ -0,0 +1,15 @@
+package com.zy.system.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+@Mapper
+@Repository
+public interface LogCleanupMapper {
+
+    int deleteExpiredBatch(@Param("table") String table,
+                           @Param("timeColumn") String timeColumn,
+                           @Param("expireDays") int expireDays,
+                           @Param("limit") int limit);
+}
diff --git a/src/main/java/com/zy/system/model/HighPrivilegeGrantStatus.java b/src/main/java/com/zy/system/model/HighPrivilegeGrantStatus.java
new file mode 100644
index 0000000..048fc6a
--- /dev/null
+++ b/src/main/java/com/zy/system/model/HighPrivilegeGrantStatus.java
@@ -0,0 +1,13 @@
+package com.zy.system.model;
+
+import lombok.Data;
+
+@Data
+public class HighPrivilegeGrantStatus {
+
+    private boolean granted;
+
+    private long remainingSeconds;
+
+    private Long expireAt;
+}
diff --git a/src/main/java/com/zy/system/model/LogCleanupExecutionResult.java b/src/main/java/com/zy/system/model/LogCleanupExecutionResult.java
new file mode 100644
index 0000000..b1a14fb
--- /dev/null
+++ b/src/main/java/com/zy/system/model/LogCleanupExecutionResult.java
@@ -0,0 +1,20 @@
+package com.zy.system.model;
+
+import lombok.Data;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Data
+public class LogCleanupExecutionResult {
+
+    private String mode;
+
+    private Integer expireDays;
+
+    private long totalDeleted;
+
+    private long executeTime;
+
+    private Map<String, Long> detail = new LinkedHashMap<>();
+}
diff --git a/src/main/java/com/zy/system/service/HighPrivilegeGrantService.java b/src/main/java/com/zy/system/service/HighPrivilegeGrantService.java
new file mode 100644
index 0000000..b125bf5
--- /dev/null
+++ b/src/main/java/com/zy/system/service/HighPrivilegeGrantService.java
@@ -0,0 +1,13 @@
+package com.zy.system.service;
+
+import com.zy.system.domain.param.HighPrivilegeGrantParam;
+import com.zy.system.model.HighPrivilegeGrantStatus;
+
+public interface HighPrivilegeGrantService {
+
+    HighPrivilegeGrantStatus grant(String token, HighPrivilegeGrantParam param);
+
+    HighPrivilegeGrantStatus getStatus(String token);
+
+    void assertGranted(String token, String actionName);
+}
diff --git a/src/main/java/com/zy/system/service/LogCleanupService.java b/src/main/java/com/zy/system/service/LogCleanupService.java
new file mode 100644
index 0000000..85685cc
--- /dev/null
+++ b/src/main/java/com/zy/system/service/LogCleanupService.java
@@ -0,0 +1,20 @@
+package com.zy.system.service;
+
+import com.zy.system.model.LogCleanupExecutionResult;
+
+import java.util.Map;
+
+public interface LogCleanupService {
+
+    Integer getExpireDays();
+
+    void saveExpireDays(Integer expireDays);
+
+    LogCleanupExecutionResult cleanupAll(Integer expireDays);
+
+    LogCleanupExecutionResult cleanupSelected(Integer expireDays, java.util.List<String> tables);
+
+    LogCleanupExecutionResult cleanupScheduled();
+
+    Map<String, String> getSupportedTables();
+}
diff --git a/src/main/java/com/zy/system/service/impl/HighPrivilegeGrantServiceImpl.java b/src/main/java/com/zy/system/service/impl/HighPrivilegeGrantServiceImpl.java
new file mode 100644
index 0000000..f4016ec
--- /dev/null
+++ b/src/main/java/com/zy/system/service/impl/HighPrivilegeGrantServiceImpl.java
@@ -0,0 +1,114 @@
+package com.zy.system.service.impl;
+
+import com.core.common.Cools;
+import com.core.exception.CoolException;
+import com.zy.common.utils.RedisUtil;
+import com.zy.core.enums.RedisKeyType;
+import com.zy.system.domain.param.HighPrivilegeGrantParam;
+import com.zy.system.entity.Role;
+import com.zy.system.entity.User;
+import com.zy.system.model.HighPrivilegeGrantStatus;
+import com.zy.system.service.HighPrivilegeGrantService;
+import com.zy.system.service.RoleService;
+import com.zy.system.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Service("highPrivilegeGrantService")
+public class HighPrivilegeGrantServiceImpl implements HighPrivilegeGrantService {
+
+    private static final String ADMIN_ROLE_CODE = "admin";
+    private static final int USER_ENABLED = 1;
+    private static final long GRANT_TTL_SECONDS = 30L * 60L;
+
+    @Autowired
+    private UserService userService;
+    @Autowired
+    private RoleService roleService;
+    @Autowired
+    private RedisUtil redisUtil;
+
+    @Override
+    public HighPrivilegeGrantStatus grant(String token, HighPrivilegeGrantParam param) {
+        if (Cools.isEmpty(token)) {
+            throw new CoolException("褰撳墠鐧诲綍宸插け鏁堬紝璇烽噸鏂扮櫥褰�");
+        }
+        if (param == null || Cools.isEmpty(param.getAccount(), param.getPassword())) {
+            throw new CoolException("璐﹀彿鍜屽瘑鐮佷笉鑳戒负绌�");
+        }
+        User user = userService.getByMobileWithSecurity(param.getAccount());
+        if (user == null) {
+            throw new CoolException("璐﹀彿鎴栧瘑鐮侀敊璇�");
+        }
+        if (!Integer.valueOf(USER_ENABLED).equals(user.getStatus())) {
+            throw new CoolException("鎺堟潈璐﹀彿宸茬鐢�");
+        }
+        Role role = roleService.getById(user.getRoleId());
+        if (role == null || !ADMIN_ROLE_CODE.equals(role.getCode())) {
+            throw new CoolException("浠卆dmin绠$悊鍛樿处鍙峰彲鑾峰彇鏈�楂樻潈闄�");
+        }
+        if (!Cools.eq(user.getPassword(), param.getPassword())) {
+            throw new CoolException("璐﹀彿鎴栧瘑鐮侀敊璇�");
+        }
+
+        long now = System.currentTimeMillis();
+        Map<String, Object> payload = new HashMap<>();
+        payload.put("account", user.getMobile());
+        payload.put("userId", user.getId());
+        payload.put("grantTime", now);
+        payload.put("expireAt", now + GRANT_TTL_SECONDS * 1000L);
+        redisUtil.set(buildRedisKey(token), payload, GRANT_TTL_SECONDS);
+        return getStatus(token);
+    }
+
+    @Override
+    public HighPrivilegeGrantStatus getStatus(String token) {
+        HighPrivilegeGrantStatus status = new HighPrivilegeGrantStatus();
+        if (Cools.isEmpty(token)) {
+            status.setGranted(false);
+            return status;
+        }
+        String redisKey = buildRedisKey(token);
+        long remainingSeconds = redisUtil.getExpire(redisKey);
+        if (remainingSeconds <= 0 || !redisUtil.hasKey(redisKey)) {
+            status.setGranted(false);
+            status.setRemainingSeconds(0L);
+            status.setExpireAt(null);
+            return status;
+        }
+        status.setGranted(true);
+        status.setRemainingSeconds(remainingSeconds);
+        Object payload = redisUtil.get(redisKey);
+        if (payload instanceof Map) {
+            Object expireAt = ((Map<?, ?>) payload).get("expireAt");
+            if (expireAt instanceof Number) {
+                status.setExpireAt(((Number) expireAt).longValue());
+            } else if (expireAt != null) {
+                try {
+                    status.setExpireAt(Long.parseLong(String.valueOf(expireAt)));
+                } catch (NumberFormatException ignore) {
+                    status.setExpireAt(System.currentTimeMillis() + remainingSeconds * 1000L);
+                }
+            }
+        }
+        if (status.getExpireAt() == null) {
+            status.setExpireAt(System.currentTimeMillis() + remainingSeconds * 1000L);
+        }
+        return status;
+    }
+
+    @Override
+    public void assertGranted(String token, String actionName) {
+        HighPrivilegeGrantStatus status = getStatus(token);
+        if (!status.isGranted()) {
+            throw new CoolException(actionName + "闇�瑕佹渶楂樻潈闄愭巿鏉冿紝璇峰厛鍦ㄥ紑鍙戜笓鐢�->绯荤粺閰嶇疆瀹屾垚鎺堟潈");
+        }
+    }
+
+    private String buildRedisKey(String token) {
+        return RedisKeyType.HIGH_PRIVILEGE_GRANT.key + token;
+    }
+}
diff --git a/src/main/java/com/zy/system/service/impl/LogCleanupServiceImpl.java b/src/main/java/com/zy/system/service/impl/LogCleanupServiceImpl.java
new file mode 100644
index 0000000..ff3943d
--- /dev/null
+++ b/src/main/java/com/zy/system/service/impl/LogCleanupServiceImpl.java
@@ -0,0 +1,168 @@
+package com.zy.system.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.core.exception.CoolException;
+import com.zy.system.entity.Config;
+import com.zy.system.mapper.LogCleanupMapper;
+import com.zy.system.model.LogCleanupExecutionResult;
+import com.zy.system.service.ConfigService;
+import com.zy.system.service.LogCleanupService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service("logCleanupService")
+public class LogCleanupServiceImpl implements LogCleanupService {
+
+    private static final String CONFIG_CODE_EXPIRE_DAYS = "logCleanupExpireDays";
+    private static final int DEFAULT_EXPIRE_DAYS = 180;
+    private static final int DELETE_BATCH_LIMIT = 50000;
+    private static final String MODE_ALL = "all";
+    private static final String MODE_SELECTED = "selected";
+    private static final Map<String, String> SUPPORTED_TABLES = new LinkedHashMap<>();
+    private static final Map<String, String> TIME_COLUMNS = new LinkedHashMap<>();
+
+    static {
+        register("asr_bas_crnp_opt", "鍫嗗灈鏈烘搷浣滄棩蹇�", "update_time");
+        register("asr_bas_dual_crnp_opt", "鍙屽伐浣嶅爢鍨涙満鎿嶄綔鏃ュ織", "update_time");
+        register("asr_bas_rgv_err_log", "RGV鏁呴殰鏃ュ織", "create_time");
+        register("asr_wrk_mast_log", "宸ヤ綔妗f棩蹇�", "modi_time");
+        register("asr_bas_dual_crnp_err_log", "鍙屽伐浣嶅爢鍨涙満鏁呴殰鏃ュ織", "create_time");
+        register("asr_bas_station_err_log", "绔欑偣鏁呴殰鏃ュ織", "create_time");
+        register("asr_bas_station_opt", "绔欑偣鎿嶄綔鏃ュ織", "update_time");
+        register("asr_wrk_analysis", "浠诲姟鎵ц鍒嗘瀽", "finish_time");
+        register("sys_http_request_log", "HTTP璇锋眰鏃ュ織", "create_time");
+        register("sys_operate_log", "鎿嶄綔鏃ュ織", "create_time");
+    }
+
+    @Autowired
+    private ConfigService configService;
+    @Autowired
+    private LogCleanupMapper logCleanupMapper;
+
+    @Override
+    public Integer getExpireDays() {
+        Config config = configService.getOne(new QueryWrapper<Config>().eq("code", CONFIG_CODE_EXPIRE_DAYS));
+        if (config == null || config.getValue() == null || config.getValue().trim().isEmpty()) {
+            return DEFAULT_EXPIRE_DAYS;
+        }
+        try {
+            int value = Integer.parseInt(config.getValue().trim());
+            return value > 0 ? value : DEFAULT_EXPIRE_DAYS;
+        } catch (NumberFormatException ignore) {
+            return DEFAULT_EXPIRE_DAYS;
+        }
+    }
+
+    @Override
+    public void saveExpireDays(Integer expireDays) {
+        int normalized = normalizeExpireDays(expireDays);
+        Config config = configService.getOne(new QueryWrapper<Config>().eq("code", CONFIG_CODE_EXPIRE_DAYS));
+        if (config == null) {
+            config = new Config();
+            config.setName("鏃ュ織娴佹按娓呯悊淇濈暀澶╂暟");
+            config.setCode(CONFIG_CODE_EXPIRE_DAYS);
+            config.setValue(String.valueOf(normalized));
+            config.setType((short) 1);
+            config.setStatus((short) 1);
+            config.setSelectType("develop");
+            configService.save(config);
+            return;
+        }
+        config.setValue(String.valueOf(normalized));
+        configService.updateById(config);
+    }
+
+    @Override
+    public LogCleanupExecutionResult cleanupAll(Integer expireDays) {
+        return executeCleanup(MODE_ALL, expireDays, new ArrayList<>(SUPPORTED_TABLES.keySet()));
+    }
+
+    @Override
+    public LogCleanupExecutionResult cleanupSelected(Integer expireDays, List<String> tables) {
+        if (tables == null || tables.isEmpty()) {
+            throw new CoolException("璇烽�夋嫨鑷冲皯涓�寮犳棩蹇楄〃");
+        }
+        LinkedHashSet<String> normalized = new LinkedHashSet<>();
+        for (String table : tables) {
+            if (table == null || table.trim().isEmpty()) {
+                continue;
+            }
+            String tableName = table.trim();
+            if (!SUPPORTED_TABLES.containsKey(tableName)) {
+                throw new CoolException("瀛樺湪涓嶆敮鎸佹竻鐞嗙殑鏃ュ織琛�: " + tableName);
+            }
+            normalized.add(tableName);
+        }
+        if (normalized.isEmpty()) {
+            throw new CoolException("璇烽�夋嫨鑷冲皯涓�寮犳棩蹇楄〃");
+        }
+        return executeCleanup(MODE_SELECTED, expireDays, new ArrayList<>(normalized));
+    }
+
+    @Override
+    public LogCleanupExecutionResult cleanupScheduled() {
+        try {
+            return cleanupAll(getExpireDays());
+        } catch (Exception ex) {
+            log.error("鏃ュ織娴佹按瀹氭椂娓呯悊澶辫触", ex);
+            throw ex;
+        }
+    }
+
+    @Override
+    public Map<String, String> getSupportedTables() {
+        return new LinkedHashMap<>(SUPPORTED_TABLES);
+    }
+
+    private LogCleanupExecutionResult executeCleanup(String mode, Integer expireDays, List<String> tables) {
+        int normalized = normalizeExpireDays(expireDays);
+        LogCleanupExecutionResult result = new LogCleanupExecutionResult();
+        result.setMode(mode);
+        result.setExpireDays(normalized);
+        result.setExecuteTime(System.currentTimeMillis());
+        long totalDeleted = 0L;
+        for (String table : tables) {
+            long tableDeleted = deleteTable(table, TIME_COLUMNS.get(table), normalized);
+            result.getDetail().put(table, tableDeleted);
+            totalDeleted += tableDeleted;
+        }
+        result.setTotalDeleted(totalDeleted);
+        return result;
+    }
+
+    private long deleteTable(String table, String timeColumn, int expireDays) {
+        long deleted = 0L;
+        while (true) {
+            int affected = logCleanupMapper.deleteExpiredBatch(table, timeColumn, expireDays, DELETE_BATCH_LIMIT);
+            if (affected <= 0) {
+                break;
+            }
+            deleted += affected;
+            if (affected < DELETE_BATCH_LIMIT) {
+                break;
+            }
+        }
+        return deleted;
+    }
+
+    private int normalizeExpireDays(Integer expireDays) {
+        if (expireDays == null || expireDays <= 0) {
+            throw new CoolException("鏃ュ織淇濈暀澶╂暟蹇呴』澶т簬0");
+        }
+        return expireDays;
+    }
+
+    private static void register(String table, String label, String timeColumn) {
+        SUPPORTED_TABLES.put(table, label);
+        TIME_COLUMNS.put(table, timeColumn);
+    }
+}
diff --git a/src/main/java/com/zy/system/timer/LogCleanupScheduler.java b/src/main/java/com/zy/system/timer/LogCleanupScheduler.java
new file mode 100644
index 0000000..715f152
--- /dev/null
+++ b/src/main/java/com/zy/system/timer/LogCleanupScheduler.java
@@ -0,0 +1,24 @@
+package com.zy.system.timer;
+
+import com.zy.system.service.LogCleanupService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class LogCleanupScheduler {
+
+    @Autowired
+    private LogCleanupService logCleanupService;
+
+    @Scheduled(cron = "0 0 23 * * ?")
+    public void cleanupExpiredLogs() {
+        try {
+            logCleanupService.cleanupScheduled();
+        } catch (Exception ex) {
+            log.error("鏃ュ織娴佹按鑷姩娓呯悊澶辫触", ex);
+        }
+    }
+}
diff --git a/src/main/resources/mapper/LogCleanupMapper.xml b/src/main/resources/mapper/LogCleanupMapper.xml
new file mode 100644
index 0000000..3b2be38
--- /dev/null
+++ b/src/main/resources/mapper/LogCleanupMapper.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.zy.system.mapper.LogCleanupMapper">
+
+    <delete id="deleteExpiredBatch">
+        DELETE FROM ${table}
+        WHERE ${timeColumn} IS NOT NULL
+          AND ${timeColumn} &lt; DATE_SUB(NOW(), INTERVAL #{expireDays} DAY)
+        LIMIT #{limit}
+    </delete>
+</mapper>
diff --git a/src/main/resources/sql/20260401_add_log_cleanup_menu_and_config.sql b/src/main/resources/sql/20260401_add_log_cleanup_menu_and_config.sql
new file mode 100644
index 0000000..4e7c485
--- /dev/null
+++ b/src/main/resources/sql/20260401_add_log_cleanup_menu_and_config.sql
@@ -0,0 +1,80 @@
+-- 鏂板 鏃ュ織娴佹按娓呯悊 鑿滃崟锛屽苟鍒濆鍖栨棩蹇椾繚鐣欏ぉ鏁伴厤缃�
+-- 璇存槑锛氭墽琛屾湰鑴氭湰鍚庯紝璇峰湪鈥滆鑹叉巿鏉冣�濋噷缁欏搴旇鑹插嬀閫夋柊鑿滃崟鍜屸�滄煡鐪嬧�濇潈闄愩��
+
+INSERT INTO sys_config(name, code, value, type, status, select_type)
+SELECT '鏃ュ織娴佹按娓呯悊淇濈暀澶╂暟', 'logCleanupExpireDays', '180', 1, 1, 'develop'
+FROM dual
+WHERE NOT EXISTS (
+    SELECT 1
+    FROM sys_config
+    WHERE code = 'logCleanupExpireDays'
+);
+
+SET @log_cleanup_parent_id := COALESCE(
+  (
+    SELECT id
+    FROM sys_resource
+    WHERE code = 'develop' AND level = 1
+    ORDER BY id
+    LIMIT 1
+  ),
+  (
+    SELECT id
+    FROM sys_resource
+    WHERE code = 'logReport' AND level = 1
+    ORDER BY id
+    LIMIT 1
+  )
+);
+
+INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
+SELECT 'logCleanup/logCleanup.html', '鏃ュ織娴佹按娓呯悊', @log_cleanup_parent_id, 2, 995, 1
+FROM dual
+WHERE @log_cleanup_parent_id IS NOT NULL
+  AND NOT EXISTS (
+    SELECT 1
+    FROM sys_resource
+    WHERE code = 'logCleanup/logCleanup.html' AND level = 2
+  );
+
+UPDATE sys_resource
+SET name = '鏃ュ織娴佹按娓呯悊',
+    resource_id = @log_cleanup_parent_id,
+    level = 2,
+    sort = 995,
+    status = 1
+WHERE code = 'logCleanup/logCleanup.html' AND level = 2;
+
+SET @log_cleanup_id := (
+  SELECT id
+  FROM sys_resource
+  WHERE code = 'logCleanup/logCleanup.html' AND level = 2
+  ORDER BY id
+  LIMIT 1
+);
+
+INSERT INTO sys_resource(code, name, resource_id, level, sort, status)
+SELECT 'logCleanup/logCleanup.html#view', '鏌ョ湅', @log_cleanup_id, 3, 1, 1
+FROM dual
+WHERE @log_cleanup_id IS NOT NULL
+  AND NOT EXISTS (
+    SELECT 1
+    FROM sys_resource
+    WHERE code = 'logCleanup/logCleanup.html#view' AND level = 3
+  );
+
+UPDATE sys_resource
+SET name = '鏌ョ湅',
+    resource_id = @log_cleanup_id,
+    level = 3,
+    sort = 1,
+    status = 1
+WHERE code = 'logCleanup/logCleanup.html#view' AND level = 3;
+
+SELECT id, code, name, resource_id, level, sort, status
+FROM sys_resource
+WHERE code IN (
+  'logCleanup/logCleanup.html',
+  'logCleanup/logCleanup.html#view'
+)
+ORDER BY level, sort, id;
diff --git a/src/main/webapp/static/js/config/config.js b/src/main/webapp/static/js/config/config.js
index d4594da..05688ee 100644
--- a/src/main/webapp/static/js/config/config.js
+++ b/src/main/webapp/static/js/config/config.js
@@ -1437,9 +1437,31 @@
                     tableHeight: 420,
                     layoutTimer: null,
                     tableResizeHandler: null,
+                    grantTimer: null,
                     dialogForm: createFormDefaults(),
                     dialogDisplay: createDisplayDefaults(),
-                    dialogRules: createFormRules()
+                    dialogRules: createFormRules(),
+                    grantStatus: {
+                        granted: false,
+                        remainingSeconds: 0,
+                        expireAt: null
+                    },
+                    grantDialog: {
+                        visible: false,
+                        submitting: false,
+                        form: {
+                            account: '',
+                            password: ''
+                        },
+                        rules: {
+                            account: [
+                                { required: true, message: '璇疯緭鍏ョ鐞嗗憳璐﹀彿', trigger: 'blur' }
+                            ],
+                            password: [
+                                { required: true, message: '璇疯緭鍏ュ瘑鐮�', trigger: 'blur' }
+                            ]
+                        }
+                    }
                 };
             },
             computed: {
@@ -1496,11 +1518,18 @@
                 },
                 isDialogReadonly: function () {
                     return this.dialog.mode === 'detail';
+                },
+                grantStatusText: function () {
+                    if (!this.grantStatus.granted) {
+                        return '褰撳墠鏃犳渶楂樻潈闄愭巿鏉�';
+                    }
+                    return '鍓╀綑 ' + this.formatRemainingSeconds(this.grantStatus.remainingSeconds);
                 }
             },
             created: function () {
                 this.fetchSelectTypeOptions();
                 this.loadTable();
+                this.loadGrantStatus();
             },
             mounted: function () {
                 var self = this;
@@ -1519,8 +1548,135 @@
                     window.removeEventListener('resize', this.tableResizeHandler);
                     this.tableResizeHandler = null;
                 }
+                this.stopGrantCountdown();
             },
             methods: $.extend({}, sharedMethods, {
+                formatRemainingSeconds: function (seconds) {
+                    var total = Number(seconds || 0);
+                    var minutes;
+                    var remainSeconds;
+                    if (total <= 0) {
+                        return '0绉�';
+                    }
+                    minutes = Math.floor(total / 60);
+                    remainSeconds = total % 60;
+                    if (minutes <= 0) {
+                        return remainSeconds + '绉�';
+                    }
+                    if (remainSeconds === 0) {
+                        return minutes + '鍒嗛挓';
+                    }
+                    return minutes + '鍒�' + remainSeconds + '绉�';
+                },
+                stopGrantCountdown: function () {
+                    if (this.grantTimer) {
+                        clearInterval(this.grantTimer);
+                        this.grantTimer = null;
+                    }
+                },
+                startGrantCountdown: function () {
+                    var self = this;
+                    self.stopGrantCountdown();
+                    if (!self.grantStatus.granted || Number(self.grantStatus.remainingSeconds) <= 0) {
+                        return;
+                    }
+                    self.grantTimer = setInterval(function () {
+                        if (!self.grantStatus.granted) {
+                            self.stopGrantCountdown();
+                            return;
+                        }
+                        if (self.grantStatus.remainingSeconds <= 1) {
+                            self.grantStatus = {
+                                granted: false,
+                                remainingSeconds: 0,
+                                expireAt: null
+                            };
+                            self.stopGrantCountdown();
+                            return;
+                        }
+                        self.grantStatus.remainingSeconds -= 1;
+                    }, 1000);
+                },
+                applyGrantStatus: function (payload) {
+                    var status = payload || {};
+                    this.grantStatus = {
+                        granted: !!status.granted,
+                        remainingSeconds: Number(status.remainingSeconds || 0),
+                        expireAt: status.expireAt || null
+                    };
+                    this.startGrantCountdown();
+                },
+                loadGrantStatus: function () {
+                    var self = this;
+                    $.ajax({
+                        url: baseUrl + '/highPrivilege/status/auth',
+                        method: 'GET',
+                        headers: self.authHeaders(),
+                        success: function (res) {
+                            if (self.handleForbidden(res)) {
+                                return;
+                            }
+                            if (!res || res.code !== 200) {
+                                return;
+                            }
+                            self.applyGrantStatus(res.data || {});
+                        }
+                    });
+                },
+                resetGrantDialog: function () {
+                    this.grantDialog.submitting = false;
+                    this.grantDialog.form = {
+                        account: '',
+                        password: ''
+                    };
+                    if (this.$refs.grantForm) {
+                        this.$refs.grantForm.clearValidate();
+                    }
+                },
+                openGrantDialog: function () {
+                    this.grantDialog.visible = true;
+                    this.$nextTick(this.resetGrantDialog);
+                },
+                submitGrant: function () {
+                    var self = this;
+                    if (!self.$refs.grantForm) {
+                        return;
+                    }
+                    self.$refs.grantForm.validate(function (valid) {
+                        if (!valid) {
+                            return false;
+                        }
+                        self.grantDialog.submitting = true;
+                        $.ajax({
+                            url: baseUrl + '/highPrivilege/grant/auth',
+                            method: 'POST',
+                            contentType: 'application/json;charset=UTF-8',
+                            headers: self.authHeaders(),
+                            data: JSON.stringify({
+                                account: self.grantDialog.form.account,
+                                password: hex_md5(self.grantDialog.form.password || '')
+                            }),
+                            success: function (res) {
+                                self.grantDialog.submitting = false;
+                                if (self.handleForbidden(res)) {
+                                    return;
+                                }
+                                if (!res || res.code !== 200) {
+                                    self.$message.error((res && res.msg) ? res.msg : '鎺堟潈澶辫触');
+                                    return;
+                                }
+                                self.$message.success('鎺堟潈鎴愬姛');
+                                self.grantDialog.visible = false;
+                                self.applyGrantStatus(res.data || {});
+                            },
+                            error: function () {
+                                self.grantDialog.submitting = false;
+                                self.$message.error('鎺堟潈澶辫触');
+                            }
+                        });
+                        return true;
+                    });
+                },
                 calculateTableHeight: function () {
                     var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 860;
                     var tableWrap = this.$refs.tableWrap;
diff --git a/src/main/webapp/static/js/logCleanup/logCleanup.js b/src/main/webapp/static/js/logCleanup/logCleanup.js
new file mode 100644
index 0000000..1c01402
--- /dev/null
+++ b/src/main/webapp/static/js/logCleanup/logCleanup.js
@@ -0,0 +1,164 @@
+(function () {
+    new Vue({
+        el: "#app",
+        data: function () {
+            return {
+                loading: false,
+                saving: false,
+                running: false,
+                tableOptions: [],
+                result: null,
+                form: {
+                    expireDays: 180,
+                    mode: "all",
+                    tables: []
+                }
+            };
+        },
+        computed: {
+            resultDetails: function () {
+                var vm = this;
+                if (!vm.result || !vm.result.detail) {
+                    return [];
+                }
+                return Object.keys(vm.result.detail).map(function (table) {
+                    return {
+                        table: table,
+                        label: vm.resolveTableLabel(table),
+                        count: vm.result.detail[table]
+                    };
+                });
+            }
+        },
+        created: function () {
+            this.loadConfig();
+        },
+        methods: {
+            authHeaders: function () {
+                return { token: localStorage.getItem("token") };
+            },
+            handleForbidden: function (res) {
+                if (res && Number(res.code) === 403) {
+                    top.location.href = baseUrl + "/";
+                    return true;
+                }
+                return false;
+            },
+            resolveTableLabel: function (table) {
+                var index;
+                for (index = 0; index < this.tableOptions.length; index += 1) {
+                    if (this.tableOptions[index].value === table) {
+                        return this.tableOptions[index].label;
+                    }
+                }
+                return table;
+            },
+            loadConfig: function () {
+                var vm = this;
+                vm.loading = true;
+                $.ajax({
+                    url: baseUrl + "/logCleanup/config/auth",
+                    method: "GET",
+                    headers: vm.authHeaders(),
+                    success: function (res) {
+                        var tables;
+                        vm.loading = false;
+                        if (vm.handleForbidden(res)) {
+                            return;
+                        }
+                        if (!res || res.code !== 200) {
+                            vm.$message.error((res && res.msg) ? res.msg : "鍔犺浇鏃ュ織娓呯悊閰嶇疆澶辫触");
+                            return;
+                        }
+                        vm.form.expireDays = Number((res.data && res.data.expireDays) || 180);
+                        tables = (res.data && res.data.tables) || {};
+                        vm.tableOptions = Object.keys(tables).map(function (key) {
+                            return {
+                                value: key,
+                                label: tables[key]
+                            };
+                        });
+                    },
+                    error: function () {
+                        vm.loading = false;
+                        vm.$message.error("鍔犺浇鏃ュ織娓呯悊閰嶇疆澶辫触");
+                    }
+                });
+            },
+            saveConfig: function () {
+                var vm = this;
+                if (!vm.form.expireDays || Number(vm.form.expireDays) <= 0) {
+                    vm.$message.warning("鏃ュ織淇濈暀澶╂暟蹇呴』澶т簬0");
+                    return;
+                }
+                vm.saving = true;
+                $.ajax({
+                    url: baseUrl + "/logCleanup/config/save/auth",
+                    method: "POST",
+                    contentType: "application/json;charset=UTF-8",
+                    headers: vm.authHeaders(),
+                    data: JSON.stringify({
+                        expireDays: Number(vm.form.expireDays)
+                    }),
+                    success: function (res) {
+                        vm.saving = false;
+                        if (vm.handleForbidden(res)) {
+                            return;
+                        }
+                        if (!res || res.code !== 200) {
+                            vm.$message.error((res && res.msg) ? res.msg : "淇濆瓨鏃ュ織娓呯悊閰嶇疆澶辫触");
+                            return;
+                        }
+                        vm.$message.success("淇濆瓨鎴愬姛");
+                    },
+                    error: function () {
+                        vm.saving = false;
+                        vm.$message.error("淇濆瓨鏃ュ織娓呯悊閰嶇疆澶辫触");
+                    }
+                });
+            },
+            runCleanup: function () {
+                var vm = this;
+                var requestBody;
+                if (vm.form.mode === "selected" && (!vm.form.tables || !vm.form.tables.length)) {
+                    vm.$message.warning("璇烽�夋嫨鑷冲皯涓�寮犳棩蹇楄〃");
+                    return;
+                }
+                requestBody = {
+                    mode: vm.form.mode,
+                    tables: vm.form.mode === "selected" ? vm.form.tables : []
+                };
+                vm.$confirm("纭畾绔嬪嵆鎵ц鏃ュ織娓呯悊鍚楋紵", "鎻愮ず", {
+                    type: "warning",
+                    confirmButtonText: "纭畾",
+                    cancelButtonText: "鍙栨秷"
+                }).then(function () {
+                    vm.running = true;
+                    $.ajax({
+                        url: baseUrl + "/logCleanup/run/auth",
+                        method: "POST",
+                        contentType: "application/json;charset=UTF-8",
+                        headers: vm.authHeaders(),
+                        data: JSON.stringify(requestBody),
+                        success: function (res) {
+                            vm.running = false;
+                            if (vm.handleForbidden(res)) {
+                                return;
+                            }
+                            if (!res || res.code !== 200) {
+                                vm.$message.error((res && res.msg) ? res.msg : "鎵嬪姩娓呯悊澶辫触");
+                                return;
+                            }
+                            vm.result = res.data || null;
+                            vm.$message.success("鎵嬪姩娓呯悊鎵ц瀹屾垚");
+                        },
+                        error: function () {
+                            vm.running = false;
+                            vm.$message.error("鎵嬪姩娓呯悊澶辫触");
+                        }
+                    });
+                }).catch(function () {});
+            }
+        }
+    });
+})();
diff --git a/src/main/webapp/static/js/wrkMast/wrkMast.js b/src/main/webapp/static/js/wrkMast/wrkMast.js
index 6e48597..a2811c7 100644
--- a/src/main/webapp/static/js/wrkMast/wrkMast.js
+++ b/src/main/webapp/static/js/wrkMast/wrkMast.js
@@ -308,7 +308,7 @@
                     cancelButtonText: "鍙栨秷"
                 }).then(function () {
                     $.ajax({
-                        url: baseUrl + "/openapi/completeTask",
+                        url: baseUrl + "/wrkMast/complete/auth",
                         contentType: "application/json",
                         headers: { token: localStorage.getItem("token") },
                         data: JSON.stringify({ wrkNo: row.wrkNo }),
@@ -339,7 +339,7 @@
                     cancelButtonText: "鍙栨秷"
                 }).then(function () {
                     $.ajax({
-                        url: baseUrl + "/openapi/cancelTask",
+                        url: baseUrl + "/wrkMast/cancel/auth",
                         contentType: "application/json",
                         headers: { token: localStorage.getItem("token") },
                         data: JSON.stringify({ wrkNo: row.wrkNo }),
diff --git a/src/main/webapp/views/config/config.html b/src/main/webapp/views/config/config.html
index ba8fc6a..46425eb 100644
--- a/src/main/webapp/views/config/config.html
+++ b/src/main/webapp/views/config/config.html
@@ -114,6 +114,7 @@
 
         .toolbar-ops {
             justify-content: flex-end;
+            align-items: center;
         }
 
         .list-toolbar .el-input__inner,
@@ -246,6 +247,15 @@
             max-height: 280px;
             overflow: auto;
             padding-right: 4px;
+        }
+
+        .grant-status-text {
+            display: inline-flex;
+            align-items: center;
+            min-height: 32px;
+            padding: 0 6px;
+            color: #5c6b7a;
+            font-size: 12px;
         }
 
         .dialog-panel .el-dialog {
@@ -468,6 +478,11 @@
                         </el-popover>
                         <el-button size="small" plain icon="el-icon-download" :loading="exporting" @click="exportRows">瀵煎嚭</el-button>
                         <el-button size="small" plain type="warning" icon="el-icon-refresh-right" @click="refreshCache">鍒锋柊缂撳瓨</el-button>
+                        <el-button size="small" plain type="success" icon="el-icon-key" @click="openGrantDialog">鏈�楂樻潈闄愭巿鏉�</el-button>
+                        <el-tag size="small" :type="grantStatus.granted ? 'success' : 'info'">
+                            {{ grantStatus.granted ? '宸叉巿鏉�' : '鏈巿鏉�' }}
+                        </el-tag>
+                        <span class="grant-status-text">{{ grantStatusText }}</span>
                     </div>
                 </div>
             </div>
@@ -717,10 +732,53 @@
             <el-button v-if="!isDialogReadonly" type="primary" :loading="dialog.submitting" @click="submitDialog">淇濆瓨</el-button>
         </div>
     </el-dialog>
+
+    <el-dialog
+        class="dialog-panel"
+        title="鏈�楂樻潈闄愭巿鏉�"
+        :visible.sync="grantDialog.visible"
+        width="460px"
+        :close-on-click-modal="false"
+        @closed="resetGrantDialog">
+        <el-alert
+            title="浠呰鑹茬紪鐮佷负admin鐨勫惎鐢ㄧ鐞嗗憳璐﹀彿鍙巿鏉冿紝鎺堟潈鏈夋晥鏈�30鍒嗛挓銆�"
+            type="warning"
+            :closable="false"
+            show-icon
+            style="margin-bottom: 16px;">
+        </el-alert>
+        <el-form
+            ref="grantForm"
+            :model="grantDialog.form"
+            :rules="grantDialog.rules"
+            label-width="90px"
+            size="small">
+            <el-form-item label="璐﹀彿" prop="account">
+                <el-input
+                    v-model.trim="grantDialog.form.account"
+                    placeholder="璇疯緭鍏ョ鐞嗗憳璐﹀彿">
+                </el-input>
+            </el-form-item>
+            <el-form-item label="瀵嗙爜" prop="password">
+                <el-input
+                    v-model="grantDialog.form.password"
+                    type="password"
+                    show-password
+                    placeholder="璇疯緭鍏ュ瘑鐮�"
+                    @keyup.enter.native="submitGrant">
+                </el-input>
+            </el-form-item>
+        </el-form>
+        <div slot="footer" class="dialog-footer">
+            <el-button @click="grantDialog.visible = false">鍙栨秷</el-button>
+            <el-button type="primary" :loading="grantDialog.submitting" @click="submitGrant">纭鎺堟潈</el-button>
+        </div>
+    </el-dialog>
 </div>
 
 <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
 <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/js/tools/md5.js"></script>
 <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
 <script type="text/javascript" src="../../static/vue/element/element.js"></script>
 <script type="text/javascript" src="../../static/js/config/config.js" charset="utf-8"></script>
diff --git a/src/main/webapp/views/logCleanup/logCleanup.html b/src/main/webapp/views/logCleanup/logCleanup.html
new file mode 100644
index 0000000..16864ef
--- /dev/null
+++ b/src/main/webapp/views/logCleanup/logCleanup.html
@@ -0,0 +1,165 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <title>鏃ュ織娴佹按娓呯悊</title>
+    <meta name="renderer" content="webkit">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+    <link rel="stylesheet" href="../../static/vue/element/element.css">
+    <link rel="stylesheet" href="../../static/css/cool.css">
+    <style>
+        [v-cloak] { display: none; }
+        html, body {
+            margin: 0;
+            min-height: 100%;
+            color: #243447;
+            font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif;
+            background:
+                radial-gradient(1000px 420px at 0% -10%, rgba(44, 107, 193, 0.12), transparent 56%),
+                radial-gradient(900px 400px at 100% 0%, rgba(28, 150, 126, 0.10), transparent 58%),
+                linear-gradient(180deg, #f2f6fb 0%, #f8fafc 100%);
+        }
+        .page-shell {
+            max-width: 1380px;
+            margin: 0 auto;
+            padding: 14px;
+            box-sizing: border-box;
+        }
+        .card-shell {
+            border-radius: 24px;
+            border: 1px solid rgba(216, 226, 238, 0.95);
+            background: rgba(255, 255, 255, 0.94);
+            box-shadow: 0 16px 32px rgba(44, 67, 96, 0.08);
+            overflow: hidden;
+        }
+        .section-head {
+            padding: 18px 20px 10px;
+            border-bottom: 1px solid rgba(222, 230, 239, 0.92);
+        }
+        .section-title {
+            margin: 0;
+            font-size: 22px;
+            font-weight: 700;
+        }
+        .section-subtitle {
+            margin-top: 8px;
+            font-size: 13px;
+            color: #6c7d90;
+        }
+        .content-wrap {
+            padding: 18px 20px 20px;
+        }
+        .config-card,
+        .result-card {
+            border: 1px solid rgba(220, 229, 239, 0.96);
+            border-radius: 20px;
+            background: rgba(255, 255, 255, 0.96);
+            padding: 18px;
+        }
+        .result-card {
+            margin-top: 16px;
+        }
+        .inline-tip {
+            font-size: 12px;
+            color: #6f7d8c;
+            margin-left: 10px;
+        }
+        .table-tag-list {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 8px;
+        }
+        @media (max-width: 900px) {
+            .page-shell {
+                padding: 10px;
+            }
+            .content-wrap,
+            .section-head {
+                padding-left: 14px;
+                padding-right: 14px;
+            }
+        }
+    </style>
+</head>
+<body>
+<div id="app" class="page-shell" v-cloak>
+    <section class="card-shell">
+        <div class="section-head">
+            <h1 class="section-title">鏃ュ織娴佹按娓呯悊</h1>
+            <div class="section-subtitle">绯荤粺鍥哄畾姣忓ぉ 23:00 鑷姩娓呯悊杩囨湡鏃ュ織銆傛墜鍔ㄦ竻鐞嗛渶瑕佸厛鍦ㄧ郴缁熼厤缃腑鑾峰彇鏈�楂樻潈闄愭巿鏉冦��</div>
+        </div>
+        <div class="content-wrap" v-loading="loading">
+            <div class="config-card">
+                <el-alert
+                    title="鎵嬪姩娓呯悊浼氭寜褰撳墠淇濈暀澶╂暟鍒犻櫎杩囨湡鏁版嵁锛屼笉浼氬垹闄や繚鐣欐湡鍐呮棩蹇椼��"
+                    type="warning"
+                    :closable="false"
+                    show-icon
+                    style="margin-bottom: 18px;">
+                </el-alert>
+                <el-form label-width="120px" size="small">
+                    <el-form-item label="淇濈暀澶╂暟">
+                        <el-input-number v-model="form.expireDays" :min="1" :step="1" controls-position="right"></el-input-number>
+                        <span class="inline-tip">甯哥敤鍊硷細180銆�360</span>
+                    </el-form-item>
+                    <el-form-item label="鑷姩娓呯悊鏃堕棿">
+                        <el-tag type="info">姣忔棩 23:00</el-tag>
+                    </el-form-item>
+                    <el-form-item label="鎵嬪姩娓呯悊妯″紡">
+                        <el-radio-group v-model="form.mode">
+                            <el-radio label="all">鎵�鏈夋棩蹇楄〃</el-radio>
+                            <el-radio label="selected">鎸囧畾鏃ュ織琛�</el-radio>
+                        </el-radio-group>
+                    </el-form-item>
+                    <el-form-item v-if="form.mode === 'selected'" label="鏃ュ織琛ㄩ�夋嫨">
+                        <el-select
+                            v-model="form.tables"
+                            multiple
+                            collapse-tags
+                            clearable
+                            filterable
+                            placeholder="璇烽�夋嫨瑕佹竻鐞嗙殑鏃ュ織琛�"
+                            style="width: 100%;">
+                            <el-option
+                                v-for="item in tableOptions"
+                                :key="item.value"
+                                :label="item.label + ' (' + item.value + ')'"
+                                :value="item.value">
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" :loading="saving" @click="saveConfig">淇濆瓨閰嶇疆</el-button>
+                        <el-button type="danger" plain :loading="running" @click="runCleanup">鎵嬪姩娓呯悊</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+
+            <div v-if="result" class="result-card">
+                <el-descriptions title="鏈�杩戜竴娆℃墜鍔ㄦ竻鐞嗙粨鏋�" :column="3" border size="small">
+                    <el-descriptions-item label="娓呯悊妯″紡">{{ result.mode === 'selected' ? '鎸囧畾鏃ュ織琛�' : '鎵�鏈夋棩蹇楄〃' }}</el-descriptions-item>
+                    <el-descriptions-item label="淇濈暀澶╂暟">{{ result.expireDays }}</el-descriptions-item>
+                    <el-descriptions-item label="鍒犻櫎鎬绘暟">{{ result.totalDeleted }}</el-descriptions-item>
+                </el-descriptions>
+                <div style="margin-top: 16px;" class="table-tag-list">
+                    <el-tag
+                        v-for="item in resultDetails"
+                        :key="item.table"
+                        type="success"
+                        effect="plain">
+                        {{ item.label }}: {{ item.count }}
+                    </el-tag>
+                </div>
+            </div>
+        </div>
+    </section>
+</div>
+
+<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
+<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
+<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
+<script type="text/javascript" src="../../static/vue/element/element.js"></script>
+<script type="text/javascript" src="../../static/js/logCleanup/logCleanup.js" charset="utf-8"></script>
+</body>
+</html>

--
Gitblit v1.9.1