Skip to content

Commit 3fb5e06

Browse files
authored
feat: add resource API (opentiny#257)
* feat: add resource API
1 parent b218d51 commit 3fb5e06

30 files changed

+3356
-18
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Copyright (c) 2023 - present TinyEngine Authors.
3+
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
4+
*
5+
* Use of this source code is governed by an MIT-style license.
6+
*
7+
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
8+
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
9+
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
10+
*
11+
*/
12+
13+
package com.tinyengine.it.task;
14+
15+
import org.springframework.boot.context.properties.ConfigurationProperties;
16+
import org.springframework.context.annotation.Configuration;
17+
import java.util.List;
18+
19+
@Configuration
20+
@ConfigurationProperties(prefix = "cleanup")
21+
public class CleanupProperties {
22+
23+
private boolean enabled = true;
24+
private boolean useTruncate = false;
25+
private List<String> whitelistTables;
26+
private String cronExpression = "0 0 0 * * ?";
27+
private boolean sendWarning = true;
28+
29+
public boolean isEnabled() { return enabled; }
30+
public void setEnabled(boolean enabled) { this.enabled = enabled; }
31+
32+
public boolean isUseTruncate() { return useTruncate; }
33+
public void setUseTruncate(boolean useTruncate) { this.useTruncate = useTruncate; }
34+
35+
public List<String> getWhitelistTables() { return whitelistTables; }
36+
public void setWhitelistTables(List<String> whitelistTables) { this.whitelistTables = whitelistTables; }
37+
38+
public String getCronExpression() { return cronExpression; }
39+
public void setCronExpression(String cronExpression) { this.cronExpression = cronExpression; }
40+
41+
public boolean isSendWarning() { return sendWarning; }
42+
public void setSendWarning(boolean sendWarning) { this.sendWarning = sendWarning; }
43+
}
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
/**
2+
* Copyright (c) 2023 - present TinyEngine Authors.
3+
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
4+
*
5+
* Use of this source code is governed by an MIT-style license.
6+
*
7+
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
8+
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
9+
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
10+
*
11+
*/
12+
13+
package com.tinyengine.it.task;
14+
15+
import jakarta.annotation.PostConstruct;
16+
import org.springframework.beans.factory.annotation.Autowired;
17+
import org.springframework.jdbc.core.JdbcTemplate;
18+
import org.springframework.scheduling.annotation.Scheduled;
19+
import org.springframework.stereotype.Service;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
import java.time.LocalDateTime;
23+
import java.time.format.DateTimeFormatter;
24+
import java.util.*;
25+
import java.util.concurrent.ConcurrentHashMap;
26+
import java.util.concurrent.atomic.AtomicInteger;
27+
@Service
28+
public class DatabaseCleanupService {
29+
30+
private static final Logger logger = LoggerFactory.getLogger(DatabaseCleanupService.class);
31+
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
32+
33+
@Autowired
34+
private JdbcTemplate jdbcTemplate;
35+
36+
@Autowired
37+
private CleanupProperties cleanupProperties;
38+
// 执行统计
39+
private final Map<String, ExecutionStats> executionStats = new ConcurrentHashMap<>();
40+
private final AtomicInteger totalExecutions = new AtomicInteger(0);
41+
42+
// 默认白名单表(如果配置文件未设置)
43+
private static final List<String> DEFAULT_TABLES = Arrays.asList(
44+
"t_resource", "t_resource_group", "r_resource_group_resource", "t_app_extension",
45+
"t_block", "t_block_carriers_relation", "t_block_group", "t_block_history",
46+
"r_material_block", "r_material_history_block", "r_block_group_block", "t_datasource",
47+
"t_i18n_entry", "t_model", "t_page", "t_page_history", "t_page_template"
48+
);
49+
50+
/**
51+
* 每天24:00自动执行清空操作
52+
*/
53+
@Scheduled(cron = "${cleanup.cron-expression:0 0 0 * * ?}")
54+
public void autoCleanupAtMidnight() {
55+
if (!cleanupProperties.isEnabled()) {
56+
logger.info("⏸️ 清空任务已禁用,跳过执行");
57+
return;
58+
}
59+
60+
String executionId = UUID.randomUUID().toString().substring(0, 8);
61+
String startTime = LocalDateTime.now().format(formatter);
62+
63+
logger.info("🎯 ======= 开始执行数据库清空任务 [{}] =======", executionId);
64+
logger.info("⏰ 执行时间: {}", startTime);
65+
logger.info("📋 目标表: {}", getWhitelistTables());
66+
67+
ExecutionStats stats = new ExecutionStats(executionId, startTime);
68+
executionStats.put(executionId, stats);
69+
totalExecutions.incrementAndGet();
70+
71+
int successCount = 0;
72+
int failedCount = 0;
73+
long totalRowsCleaned = 0;
74+
75+
for (String tableName : getWhitelistTables()) {
76+
try {
77+
validateTableName(tableName);
78+
79+
if (!tableExists(tableName)) {
80+
logger.warn("⚠️ 表 {} 不存在,跳过", tableName);
81+
stats.recordSkipped(tableName, "表不存在");
82+
continue;
83+
}
84+
85+
long beforeCount = getTableRecordCount(tableName);
86+
long rowsCleaned;
87+
88+
if (cleanupProperties.isUseTruncate()) {
89+
truncateTable(tableName);
90+
rowsCleaned = beforeCount; // TRUNCATE会清空所有数据
91+
} else {
92+
rowsCleaned = clearTableData(tableName);
93+
}
94+
95+
totalRowsCleaned += rowsCleaned;
96+
successCount++;
97+
98+
logger.info("✅ 表 {} 清空完成: 删除 {} 条记录", tableName, rowsCleaned);
99+
stats.recordSuccess(tableName, rowsCleaned);
100+
101+
} catch (Exception e) {
102+
failedCount++;
103+
logger.error("❌ 表 {} 清空失败: {}", tableName, e.getMessage(), e);
104+
stats.recordFailure(tableName, e.getMessage());
105+
}
106+
}
107+
108+
String endTime = LocalDateTime.now().format(formatter);
109+
stats.setEndTime(endTime);
110+
stats.setTotalRowsCleaned(totalRowsCleaned);
111+
112+
logger.info("📊 ======= 任务完成统计 [{}] =======", executionId);
113+
logger.info("✅ 成功表数: {}", successCount);
114+
logger.info("❌ 失败表数: {}", failedCount);
115+
logger.info("📈 总共删除记录: {}", totalRowsCleaned);
116+
logger.info("⏰ 耗时: {} 秒", stats.getDurationSeconds());
117+
logger.info("🕐 开始: {}, 结束: {}", startTime, endTime);
118+
logger.info("🎉 ======= 任务执行完成 =======\n");
119+
}
120+
121+
/**
122+
* 每天23:55发送预警通知
123+
*/
124+
@Scheduled(cron = "0 55 23 * * ?")
125+
public void sendCleanupWarning() {
126+
if (!cleanupProperties.isEnabled() || !cleanupProperties.isSendWarning()) {
127+
return;
128+
}
129+
130+
logger.warn("⚠️ ⚠️ ⚠️ 重要通知:5分钟后将自动清空数据库表!");
131+
logger.warn("📋 目标表: {}", getWhitelistTables());
132+
logger.warn("⏰ 执行时间: 00:00:00");
133+
logger.warn("💡 如需取消,请修改配置: cleanup.enabled=false");
134+
logger.warn("==========================================");
135+
}
136+
137+
/**
138+
* 应用启动时初始化
139+
*/
140+
@PostConstruct
141+
public void init() {
142+
logger.info("🚀 数据库自动清空服务初始化完成");
143+
logger.info("📋 配置表: {}", getWhitelistTables());
144+
logger.info("⏰ 执行时间: {}", cleanupProperties.getCronExpression());
145+
logger.info("🔧 使用模式: {}", cleanupProperties.isUseTruncate() ? "TRUNCATE" : "DELETE");
146+
logger.info("✅ 服务状态: {}", cleanupProperties.isEnabled() ? "已启用" : "已禁用");
147+
logger.info("==========================================");
148+
}
149+
150+
/**
151+
* 获取白名单表列表
152+
*/
153+
public List<String> getWhitelistTables() {
154+
List<String> tables = cleanupProperties.getWhitelistTables();
155+
return tables != null && !tables.isEmpty() ? tables : DEFAULT_TABLES;
156+
}
157+
158+
/**
159+
* 清空表数据(DELETE方式)
160+
*/
161+
private long clearTableData(String tableName) {
162+
validateTableName(tableName);
163+
String sql = "DELETE FROM " + tableName;
164+
int affectedRows = jdbcTemplate.update(sql);
165+
return affectedRows;
166+
}
167+
168+
/**
169+
* 清空表数据(TRUNCATE方式)
170+
*/
171+
private void truncateTable(String tableName) {
172+
validateTableName(tableName);
173+
String sql = "TRUNCATE TABLE " + tableName;
174+
jdbcTemplate.execute(sql);
175+
}
176+
177+
/**
178+
* 检查表是否存在
179+
*/
180+
public boolean tableExists(String tableName) {
181+
try {
182+
String sql = "SELECT COUNT(*) FROM information_schema.tables " +
183+
"WHERE table_schema = DATABASE() AND table_name = ?";
184+
Integer count = jdbcTemplate.queryForObject(sql, Integer.class, tableName.toUpperCase());
185+
return count != null && count > 0;
186+
} catch (Exception e) {
187+
logger.warn("检查表存在失败: {}", e.getMessage());
188+
return false;
189+
}
190+
}
191+
192+
/**
193+
* 获取表记录数量
194+
*/
195+
public long getTableRecordCount(String tableName) {
196+
try {
197+
validateTableName(tableName);
198+
String sql = "SELECT COUNT(*) FROM " + tableName;
199+
Long count = jdbcTemplate.queryForObject(sql, Long.class);
200+
return count != null ? count : 0;
201+
} catch (Exception e) {
202+
logger.error("获取表记录数失败: {}", e.getMessage());
203+
return -1;
204+
}
205+
}
206+
207+
/**
208+
* 验证表名安全性
209+
*/
210+
private void validateTableName(String tableName) {
211+
if (tableName == null || tableName.trim().isEmpty()) {
212+
throw new IllegalArgumentException("表名不能为空");
213+
}
214+
if (!tableName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
215+
throw new IllegalArgumentException("无效的表名格式: " + tableName);
216+
}
217+
}
218+
219+
/**
220+
* 获取执行统计
221+
*/
222+
public Map<String, ExecutionStats> getExecutionStats() {
223+
return new LinkedHashMap<>(executionStats);
224+
}
225+
226+
public int getTotalExecutions() {
227+
return totalExecutions.get();
228+
}
229+
230+
/**
231+
* 执行统计内部类
232+
*/
233+
public static class ExecutionStats {
234+
private final String executionId;
235+
private final String startTime;
236+
private String endTime;
237+
private long totalRowsCleaned;
238+
private final Map<String, TableResult> tableResults = new LinkedHashMap<>();
239+
240+
public ExecutionStats(String executionId, String startTime) {
241+
this.executionId = executionId;
242+
this.startTime = startTime;
243+
}
244+
245+
public void recordSuccess(String tableName, long rowsCleaned) {
246+
tableResults.put(tableName, new TableResult("SUCCESS", rowsCleaned, null));
247+
}
248+
249+
public void recordFailure(String tableName, String errorMessage) {
250+
tableResults.put(tableName, new TableResult("FAILED", 0, errorMessage));
251+
}
252+
253+
public void recordSkipped(String tableName, String reason) {
254+
tableResults.put(tableName, new TableResult("SKIPPED", 0, reason));
255+
}
256+
257+
// Getters and setters
258+
public String getExecutionId() { return executionId; }
259+
public String getStartTime() { return startTime; }
260+
public String getEndTime() { return endTime; }
261+
public void setEndTime(String endTime) { this.endTime = endTime; }
262+
public long getTotalRowsCleaned() { return totalRowsCleaned; }
263+
public void setTotalRowsCleaned(long totalRowsCleaned) { this.totalRowsCleaned = totalRowsCleaned; }
264+
public Map<String, TableResult> getTableResults() { return tableResults; }
265+
266+
public long getDurationSeconds() {
267+
if (startTime != null && endTime != null) {
268+
LocalDateTime start = LocalDateTime.parse(startTime, formatter);
269+
LocalDateTime end = LocalDateTime.parse(endTime, formatter);
270+
return java.time.Duration.between(start, end).getSeconds();
271+
}
272+
return 0;
273+
}
274+
}
275+
276+
/**
277+
* 表结果内部类
278+
*/
279+
public static class TableResult {
280+
private final String status;
281+
private final long rowsCleaned;
282+
private final String message;
283+
284+
public TableResult(String status, long rowsCleaned, String message) {
285+
this.status = status;
286+
this.rowsCleaned = rowsCleaned;
287+
this.message = message;
288+
}
289+
290+
// Getters
291+
public String getStatus() { return status; }
292+
public long getRowsCleaned() { return rowsCleaned; }
293+
public String getMessage() { return message; }
294+
}
295+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright (c) 2023 - present TinyEngine Authors.
3+
* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd.
4+
*
5+
* Use of this source code is governed by an MIT-style license.
6+
*
7+
* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL,
8+
* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
9+
* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS.
10+
*
11+
*/
12+
13+
package com.tinyengine.it.task;
14+
15+
import org.springframework.context.annotation.Bean;
16+
import org.springframework.context.annotation.Configuration;
17+
import org.springframework.scheduling.TaskScheduler;
18+
import org.springframework.scheduling.annotation.EnableScheduling;
19+
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
20+
21+
@Configuration
22+
@EnableScheduling
23+
public class SchedulerConfig {
24+
// 启用Spring定时任务功能
25+
@Bean
26+
public TaskScheduler taskScheduler() {
27+
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
28+
scheduler.setPoolSize(5);
29+
scheduler.setThreadNamePrefix("cleanup-scheduler-");
30+
return scheduler;
31+
}
32+
}

0 commit comments

Comments
 (0)