feat(log): 优化操作日志记录和展示功能
- 在OperationLogEntity实体中新增summary和ipAddress字段存储摘要和IP信息 - 修改operationLogService.record方法支持传入操作摘要信息 - 实现客户端IP地址解析功能,支持X-Forwarded-For和X-Real-IP头 - 更新系统更新服务中的数据库表结构迁移逻辑,增加NOT NULL列处理 - 优化前端操作日志页面展示,添加标签分类和详情弹窗功能 - 在系统更新流式响应中增加网络连接异常处理机制 - 添加Nginx代理配置中的缓冲区设置以支持实时日志流式传输
这个提交包含在:
父节点
50da70d580
当前提交
f9ad40cb98
@ -106,9 +106,10 @@ public class AppController {
|
|||||||
TenantEntity tenant = tenantRepository.findById(tenantId)
|
TenantEntity tenant = tenantRepository.findById(tenantId)
|
||||||
.orElseThrow(() -> new RuntimeException("Tenant not found"));
|
.orElseThrow(() -> new RuntimeException("Tenant not found"));
|
||||||
emailService.sendVerificationCode(tenant.getEmail(), purpose);
|
emailService.sendVerificationCode(tenant.getEmail(), purpose);
|
||||||
operationLogService.record(tenantId, "APP", "APP_SECRET", appKey, "REQUEST_SECRET_VERIFY", Map.of(
|
String purposeLabel = "REVEAL_SECRET".equals(purpose) ? "查看密钥" : "重置密钥";
|
||||||
"purpose", purpose
|
operationLogService.record(tenantId, "APP", "APP_SECRET", appKey, "REQUEST_SECRET_VERIFY",
|
||||||
));
|
"申请" + purposeLabel + "验证码,应用 " + appKey,
|
||||||
|
Map.of("purpose", purpose));
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,9 +123,9 @@ public class AppController {
|
|||||||
TenantEntity tenant = tenantRepository.findById(tenantId)
|
TenantEntity tenant = tenantRepository.findById(tenantId)
|
||||||
.orElseThrow(() -> new RuntimeException("Tenant not found"));
|
.orElseThrow(() -> new RuntimeException("Tenant not found"));
|
||||||
emailService.verify(tenant.getEmail(), body.get("code"), "REVEAL_SECRET");
|
emailService.verify(tenant.getEmail(), body.get("code"), "REVEAL_SECRET");
|
||||||
operationLogService.record(tenantId, "APP", "APP_SECRET", appKey, "REVEAL_APP_SECRET", Map.of(
|
operationLogService.record(tenantId, "APP", "APP_SECRET", appKey, "REVEAL_APP_SECRET",
|
||||||
"appKey", app.getAppKey()
|
"查看应用「" + app.getName() + "」的 AppSecret",
|
||||||
));
|
Map.of("appKey", app.getAppKey()));
|
||||||
return ResponseEntity.ok(ApiResponse.success(Map.of("appSecret", app.getAppSecret())));
|
return ResponseEntity.ok(ApiResponse.success(Map.of("appSecret", app.getAppSecret())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -65,10 +65,9 @@ public class FeatureServiceController {
|
|||||||
throw new com.xuqm.common.exception.BusinessException(400, "开启服务请通过 request-activation 申请");
|
throw new com.xuqm.common.exception.BusinessException(400, "开启服务请通过 request-activation 申请");
|
||||||
}
|
}
|
||||||
FeatureServiceEntity saved = featureServiceManager.disable(appKey, platform, serviceType);
|
FeatureServiceEntity saved = featureServiceManager.disable(appKey, platform, serviceType);
|
||||||
operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", saved.getId(), "DISABLE_SERVICE", java.util.Map.of(
|
operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", saved.getId(), "DISABLE_SERVICE",
|
||||||
"platform", platform.name(),
|
"停用 " + platform.name() + " 平台的 " + serviceType.name() + " 服务",
|
||||||
"serviceType", serviceType.name()
|
java.util.Map.of("platform", platform.name(), "serviceType", serviceType.name()));
|
||||||
));
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(saved));
|
return ResponseEntity.ok(ApiResponse.success(saved));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,10 +117,9 @@ public class FeatureServiceController {
|
|||||||
};
|
};
|
||||||
FeatureServiceEntity saved = featureServiceManager.updateConfig(
|
FeatureServiceEntity saved = featureServiceManager.updateConfig(
|
||||||
appKey, platform, serviceType, config);
|
appKey, platform, serviceType, config);
|
||||||
operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", saved.getId(), "UPDATE_SERVICE_CONFIG", java.util.Map.of(
|
operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", saved.getId(), "UPDATE_SERVICE_CONFIG",
|
||||||
"platform", platform.name(),
|
"更新 " + platform.name() + " 平台 " + serviceType.name() + " 服务配置",
|
||||||
"serviceType", serviceType.name()
|
java.util.Map.of("platform", platform.name(), "serviceType", serviceType.name()));
|
||||||
));
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(saved));
|
return ResponseEntity.ok(ApiResponse.success(saved));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +139,9 @@ public class FeatureServiceController {
|
|||||||
if (applyReason != null && !applyReason.isBlank()) {
|
if (applyReason != null && !applyReason.isBlank()) {
|
||||||
detail.put("applyReason", applyReason);
|
detail.put("applyReason", applyReason);
|
||||||
}
|
}
|
||||||
operationLogService.record(tenantId, "SERVICE", "SERVICE_ACTIVATION", saved.getId(), "REQUEST_SERVICE_ACTIVATION", detail);
|
operationLogService.record(tenantId, "SERVICE", "SERVICE_ACTIVATION", saved.getId(), "REQUEST_SERVICE_ACTIVATION",
|
||||||
|
"申请开通 " + platform.name() + " 平台 " + serviceType.name() + " 服务",
|
||||||
|
detail);
|
||||||
return ResponseEntity.ok(ApiResponse.success(saved));
|
return ResponseEntity.ok(ApiResponse.success(saved));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,10 +159,9 @@ public class FeatureServiceController {
|
|||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
appService.getByAppKey(appKey, tenantId);
|
appService.getByAppKey(appKey, tenantId);
|
||||||
FeatureServiceEntity updated = featureServiceManager.regenerateSecretKey(id);
|
FeatureServiceEntity updated = featureServiceManager.regenerateSecretKey(id);
|
||||||
operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", updated.getId(), "REGENERATE_KEY", java.util.Map.of(
|
operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", updated.getId(), "REGENERATE_KEY",
|
||||||
"platform", updated.getPlatform().name(),
|
"重新生成 " + updated.getPlatform().name() + " 平台 " + updated.getServiceType().name() + " 服务密钥",
|
||||||
"serviceType", updated.getServiceType().name()
|
java.util.Map.of("platform", updated.getPlatform().name(), "serviceType", updated.getServiceType().name()));
|
||||||
));
|
|
||||||
return ResponseEntity.ok(ApiResponse.success(updated));
|
return ResponseEntity.ok(ApiResponse.success(updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,9 +46,9 @@ public class SubAccountController {
|
|||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
requireNonBlank(email, "email");
|
requireNonBlank(email, "email");
|
||||||
emailService.sendVerificationCode(email, "SUB_ACCOUNT");
|
emailService.sendVerificationCode(email, "SUB_ACCOUNT");
|
||||||
operationLogService.record(tenantId, "SUB_ACCOUNT", "EMAIL_VERIFY", email, "SEND_VERIFY_CODE", Map.of(
|
operationLogService.record(tenantId, "SUB_ACCOUNT", "EMAIL_VERIFY", email, "SEND_VERIFY_CODE",
|
||||||
"email", email
|
"发送子账号邮箱验证码到 " + email,
|
||||||
));
|
Map.of("email", email));
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,9 +59,9 @@ public class SubAccountController {
|
|||||||
requireNonBlank(email, "email");
|
requireNonBlank(email, "email");
|
||||||
requireNonBlank(code, "code");
|
requireNonBlank(code, "code");
|
||||||
subAccountService.verifyEmail(tenantId, email, code);
|
subAccountService.verifyEmail(tenantId, email, code);
|
||||||
operationLogService.record(tenantId, "SUB_ACCOUNT", "EMAIL_VERIFY", email, "VERIFY_EMAIL", Map.of(
|
operationLogService.record(tenantId, "SUB_ACCOUNT", "EMAIL_VERIFY", email, "VERIFY_EMAIL",
|
||||||
"email", email
|
"验证子账号邮箱 " + email + " 成功",
|
||||||
));
|
Map.of("email", email));
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -103,6 +103,8 @@ public class SystemUpdateController {
|
|||||||
});
|
});
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.contentType(MediaType.TEXT_PLAIN)
|
.contentType(MediaType.TEXT_PLAIN)
|
||||||
|
.header("X-Accel-Buffering", "no")
|
||||||
|
.header("Cache-Control", "no-cache, no-store")
|
||||||
.body(body);
|
.body(body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,12 @@ public class OperationLogEntity {
|
|||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String detailJson;
|
private String detailJson;
|
||||||
|
|
||||||
|
@Column(length = 255)
|
||||||
|
private String summary;
|
||||||
|
|
||||||
|
@Column(length = 64)
|
||||||
|
private String ipAddress;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
@ -66,6 +72,12 @@ public class OperationLogEntity {
|
|||||||
public String getDetailJson() { return detailJson; }
|
public String getDetailJson() { return detailJson; }
|
||||||
public void setDetailJson(String detailJson) { this.detailJson = detailJson; }
|
public void setDetailJson(String detailJson) { this.detailJson = detailJson; }
|
||||||
|
|
||||||
|
public String getSummary() { return summary; }
|
||||||
|
public void setSummary(String summary) { this.summary = summary; }
|
||||||
|
|
||||||
|
public String getIpAddress() { return ipAddress; }
|
||||||
|
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,11 +82,9 @@ public class AppService {
|
|||||||
app.setLicenseFileContent(generateLicenseFileContent(app));
|
app.setLicenseFileContent(generateLicenseFileContent(app));
|
||||||
AppEntity saved = appRepository.save(app);
|
AppEntity saved = appRepository.save(app);
|
||||||
autoEnableFileService(saved.getAppKey());
|
autoEnableFileService(saved.getAppKey());
|
||||||
operationLogService.record(tenantId, "APP", "APP", saved.getAppKey(), "CREATE_APP", Map.of(
|
operationLogService.record(tenantId, "APP", "APP", saved.getAppKey(), "CREATE_APP",
|
||||||
"name", saved.getName(),
|
"创建应用「" + saved.getName() + "」(" + saved.getPackageName() + ")",
|
||||||
"packageName", saved.getPackageName(),
|
Map.of("name", saved.getName(), "packageName", saved.getPackageName(), "appKey", saved.getAppKey()));
|
||||||
"appKey", saved.getAppKey()
|
|
||||||
));
|
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,21 +119,18 @@ public class AppService {
|
|||||||
after.put("packageName", saved.getPackageName());
|
after.put("packageName", saved.getPackageName());
|
||||||
after.put("description", saved.getDescription());
|
after.put("description", saved.getDescription());
|
||||||
after.put("iconUrl", saved.getIconUrl());
|
after.put("iconUrl", saved.getIconUrl());
|
||||||
operationLogService.record(tenantId, "APP", "APP", saved.getAppKey(), "UPDATE_APP", Map.of(
|
operationLogService.record(tenantId, "APP", "APP", saved.getAppKey(), "UPDATE_APP",
|
||||||
"before", before,
|
"更新应用「" + saved.getName() + "」的信息",
|
||||||
"after", after
|
Map.of("before", before, "after", after));
|
||||||
));
|
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete(String appKey, String tenantId) {
|
public void delete(String appKey, String tenantId) {
|
||||||
AppEntity app = getByAppKey(appKey, tenantId);
|
AppEntity app = getByAppKey(appKey, tenantId);
|
||||||
appRepository.delete(app);
|
appRepository.delete(app);
|
||||||
operationLogService.record(tenantId, "APP", "APP", app.getAppKey(), "DELETE_APP", Map.of(
|
operationLogService.record(tenantId, "APP", "APP", app.getAppKey(), "DELETE_APP",
|
||||||
"name", app.getName(),
|
"删除应用「" + app.getName() + "」(" + app.getPackageName() + ")",
|
||||||
"packageName", app.getPackageName(),
|
Map.of("name", app.getName(), "packageName", app.getPackageName(), "appKey", app.getAppKey()));
|
||||||
"appKey", app.getAppKey()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String resetSecret(String appKey, String tenantId) {
|
public String resetSecret(String appKey, String tenantId) {
|
||||||
@ -143,11 +138,9 @@ public class AppService {
|
|||||||
String newSecret = generateSecret();
|
String newSecret = generateSecret();
|
||||||
app.setAppSecret(newSecret);
|
app.setAppSecret(newSecret);
|
||||||
appRepository.save(app);
|
appRepository.save(app);
|
||||||
operationLogService.record(tenantId, "APP", "APP_SECRET", app.getAppKey(), "RESET_APP_SECRET", Map.of(
|
operationLogService.record(tenantId, "APP", "APP_SECRET", app.getAppKey(), "RESET_APP_SECRET",
|
||||||
"name", app.getName(),
|
"重置应用「" + app.getName() + "」的 AppSecret",
|
||||||
"packageName", app.getPackageName(),
|
Map.of("name", app.getName(), "packageName", app.getPackageName(), "appKey", app.getAppKey()));
|
||||||
"appKey", app.getAppKey()
|
|
||||||
));
|
|
||||||
return newSecret;
|
return newSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,8 @@ public class DashboardService {
|
|||||||
result.put("serviceCount", serviceCount);
|
result.put("serviceCount", serviceCount);
|
||||||
result.put("subAccountCount", subAccountCount);
|
result.put("subAccountCount", subAccountCount);
|
||||||
|
|
||||||
operationLogService.record(tenantId, "CONSOLE", "DASHBOARD", tenantId, "VIEW_DASHBOARD", result);
|
operationLogService.record(tenantId, "CONSOLE", "DASHBOARD", tenantId, "VIEW_DASHBOARD",
|
||||||
|
"查看控制台概览", result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,11 +3,14 @@ package com.xuqm.tenant.service;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.xuqm.tenant.entity.OperationLogEntity;
|
import com.xuqm.tenant.entity.OperationLogEntity;
|
||||||
import com.xuqm.tenant.repository.OperationLogRepository;
|
import com.xuqm.tenant.repository.OperationLogRepository;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@ -31,6 +34,16 @@ public class OperationLogService {
|
|||||||
String resourceId,
|
String resourceId,
|
||||||
String action,
|
String action,
|
||||||
Map<String, Object> detail) {
|
Map<String, Object> detail) {
|
||||||
|
record(tenantId, moduleType, resourceType, resourceId, action, null, detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void record(String tenantId,
|
||||||
|
String moduleType,
|
||||||
|
String resourceType,
|
||||||
|
String resourceId,
|
||||||
|
String action,
|
||||||
|
String summary,
|
||||||
|
Map<String, Object> detail) {
|
||||||
OperationLogEntity entity = new OperationLogEntity();
|
OperationLogEntity entity = new OperationLogEntity();
|
||||||
entity.setId(UUID.randomUUID().toString());
|
entity.setId(UUID.randomUUID().toString());
|
||||||
entity.setTenantId(tenantId);
|
entity.setTenantId(tenantId);
|
||||||
@ -39,6 +52,8 @@ public class OperationLogService {
|
|||||||
entity.setResourceId(resourceId);
|
entity.setResourceId(resourceId);
|
||||||
entity.setAction(action);
|
entity.setAction(action);
|
||||||
entity.setOperator(currentOperator());
|
entity.setOperator(currentOperator());
|
||||||
|
entity.setSummary(summary);
|
||||||
|
entity.setIpAddress(resolveClientIp());
|
||||||
entity.setDetailJson(serialize(detail));
|
entity.setDetailJson(serialize(detail));
|
||||||
entity.setCreatedAt(LocalDateTime.now());
|
entity.setCreatedAt(LocalDateTime.now());
|
||||||
repository.save(entity);
|
repository.save(entity);
|
||||||
@ -65,6 +80,27 @@ public class OperationLogService {
|
|||||||
return auth.getName();
|
return auth.getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveClientIp() {
|
||||||
|
try {
|
||||||
|
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attrs == null) return null;
|
||||||
|
HttpServletRequest request = attrs.getRequest();
|
||||||
|
String ip = request.getHeader("X-Forwarded-For");
|
||||||
|
if (ip != null && !ip.isBlank()) {
|
||||||
|
ip = ip.split(",")[0].trim();
|
||||||
|
}
|
||||||
|
if (ip == null || ip.isBlank()) {
|
||||||
|
ip = request.getHeader("X-Real-IP");
|
||||||
|
}
|
||||||
|
if (ip == null || ip.isBlank()) {
|
||||||
|
ip = request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String serialize(Map<String, Object> detail) {
|
private String serialize(Map<String, Object> detail) {
|
||||||
try {
|
try {
|
||||||
Map<String, Object> payload = detail == null ? Map.of() : new LinkedHashMap<>(detail);
|
Map<String, Object> payload = detail == null ? Map.of() : new LinkedHashMap<>(detail);
|
||||||
|
|||||||
@ -64,11 +64,9 @@ public class SubAccountService {
|
|||||||
sub.setParentId(parentId);
|
sub.setParentId(parentId);
|
||||||
sub.setCreatedAt(LocalDateTime.now());
|
sub.setCreatedAt(LocalDateTime.now());
|
||||||
TenantEntity saved = tenantRepository.save(sub);
|
TenantEntity saved = tenantRepository.save(sub);
|
||||||
operationLogService.record(parentId, "SUB_ACCOUNT", "SUB_ACCOUNT", saved.getId(), "CREATE_SUB_ACCOUNT", Map.of(
|
operationLogService.record(parentId, "SUB_ACCOUNT", "SUB_ACCOUNT", saved.getId(), "CREATE_SUB_ACCOUNT",
|
||||||
"username", saved.getUsername(),
|
"创建子账号「" + saved.getNickname() + "」(" + saved.getUsername() + ")",
|
||||||
"nickname", saved.getNickname(),
|
Map.of("username", saved.getUsername(), "nickname", saved.getNickname(), "email", saved.getEmail()));
|
||||||
"email", saved.getEmail()
|
|
||||||
));
|
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,11 +82,9 @@ public class SubAccountService {
|
|||||||
}
|
}
|
||||||
sub.setStatus(TenantEntity.Status.DISABLED);
|
sub.setStatus(TenantEntity.Status.DISABLED);
|
||||||
tenantRepository.save(sub);
|
tenantRepository.save(sub);
|
||||||
operationLogService.record(parentId, "SUB_ACCOUNT", "SUB_ACCOUNT", sub.getId(), "DISABLE_SUB_ACCOUNT", Map.of(
|
operationLogService.record(parentId, "SUB_ACCOUNT", "SUB_ACCOUNT", sub.getId(), "DISABLE_SUB_ACCOUNT",
|
||||||
"username", sub.getUsername(),
|
"禁用子账号「" + sub.getNickname() + "」(" + sub.getUsername() + ")",
|
||||||
"nickname", sub.getNickname(),
|
Map.of("username", sub.getUsername(), "nickname", sub.getNickname(), "email", sub.getEmail()));
|
||||||
"email", sub.getEmail()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String generatePassword() {
|
public String generatePassword() {
|
||||||
|
|||||||
@ -177,6 +177,10 @@ public class SystemUpdateService {
|
|||||||
PRESERVE_TABLES.put("t_risk_config", "id");
|
PRESERVE_TABLES.put("t_risk_config", "id");
|
||||||
PRESERVE_TABLES.put("app_licenses", "app_key");
|
PRESERVE_TABLES.put("app_licenses", "app_key");
|
||||||
PRESERVE_TABLES.put("t_sensitive_word", "id");
|
PRESERVE_TABLES.put("t_sensitive_word", "id");
|
||||||
|
PRESERVE_TABLES.put("t_operation_log", "id");
|
||||||
|
PRESERVE_TABLES.put("t_service_activation_request","id");
|
||||||
|
PRESERVE_TABLES.put("t_email_verification", "id");
|
||||||
|
PRESERVE_TABLES.put("t_migrate_key", "id");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetDatabaseSchema(Consumer<String> emit) {
|
private void resetDatabaseSchema(Consumer<String> emit) {
|
||||||
@ -297,9 +301,23 @@ public class SystemUpdateService {
|
|||||||
log.warn("no common columns between {} and {}", table, tmpTable);
|
log.warn("no common columns between {} and {}", table, tmpTable);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// 获取目标表列的 NOT NULL 和 DEFAULT 信息
|
||||||
|
Map<String, Map<String, String>> colMeta = getColumnMeta(stmt, table);
|
||||||
|
// 对 NOT NULL 列用 COALESCE 兜底,避免 INSERT IGNORE 静默丢行
|
||||||
|
List<String> selectExprs = new java.util.ArrayList<>();
|
||||||
|
for (String col : columns) {
|
||||||
|
Map<String, String> meta = colMeta.get(col);
|
||||||
|
if (meta != null && "NO".equals(meta.get("nullable"))) {
|
||||||
|
String fallback = guessNotNullDefault(meta);
|
||||||
|
selectExprs.add("COALESCE(`" + col + "`, " + fallback + ") AS `" + col + "`");
|
||||||
|
} else {
|
||||||
|
selectExprs.add("`" + col + "`");
|
||||||
|
}
|
||||||
|
}
|
||||||
String colList = columns.stream().map(c -> "`" + c + "`").collect(Collectors.joining(", "));
|
String colList = columns.stream().map(c -> "`" + c + "`").collect(Collectors.joining(", "));
|
||||||
String sql = "INSERT IGNORE INTO `" + table + "` (" + colList + ") " +
|
String selectList = String.join(", ", selectExprs);
|
||||||
"SELECT " + colList + " FROM `" + tmpTable + "`";
|
String sql = "INSERT INTO `" + table + "` (" + colList + ") " +
|
||||||
|
"SELECT " + selectList + " FROM `" + tmpTable + "`";
|
||||||
int rows = stmt.executeUpdate(sql);
|
int rows = stmt.executeUpdate(sql);
|
||||||
log.info("restored {} rows into {}", rows, table);
|
log.info("restored {} rows into {}", rows, table);
|
||||||
|
|
||||||
@ -331,6 +349,44 @@ public class SystemUpdateService {
|
|||||||
return common;
|
return common;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取目标表各列的 Null、Default 等元信息。 */
|
||||||
|
private Map<String, Map<String, String>> getColumnMeta(Statement stmt, String table) throws Exception {
|
||||||
|
Map<String, Map<String, String>> meta = new java.util.LinkedHashMap<>();
|
||||||
|
try (ResultSet rs = stmt.executeQuery("SHOW COLUMNS FROM `" + table + "`")) {
|
||||||
|
while (rs.next()) {
|
||||||
|
Map<String, String> info = new java.util.LinkedHashMap<>();
|
||||||
|
info.put("type", rs.getString("Type"));
|
||||||
|
info.put("nullable", rs.getString("Null"));
|
||||||
|
info.put("default", rs.getString("Default"));
|
||||||
|
meta.put(rs.getString("Field"), info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 为 NOT NULL 且无 DEFAULT 的列根据类型推断一个合理的兜底值。 */
|
||||||
|
private String guessNotNullDefault(Map<String, String> meta) {
|
||||||
|
String defaultVal = meta.get("default");
|
||||||
|
if (defaultVal != null && !"null".equalsIgnoreCase(defaultVal) && !defaultVal.isEmpty()) {
|
||||||
|
// MySQL SHOW COLUMNS 的 Default 字段已经是不带引号的字面量
|
||||||
|
// 但字符串类型的值在 JDBC ResultSet 中返回时可能不带引号,需要加上
|
||||||
|
String type = meta.getOrDefault("type", "").toLowerCase();
|
||||||
|
if (type.startsWith("varchar") || type.startsWith("char") || type.startsWith("text") || type.startsWith("enum")) {
|
||||||
|
return "'" + defaultVal.replace("'", "\\'") + "'";
|
||||||
|
}
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
String type = meta.getOrDefault("type", "").toLowerCase();
|
||||||
|
if (type.startsWith("varchar") || type.startsWith("char") || type.startsWith("text")) return "''";
|
||||||
|
if (type.startsWith("bigint") || type.startsWith("int") || type.startsWith("tinyint") || type.startsWith("smallint")) return "0";
|
||||||
|
if (type.startsWith("decimal") || type.startsWith("double") || type.startsWith("float")) return "0";
|
||||||
|
if (type.startsWith("datetime") || type.startsWith("timestamp")) return "CURRENT_TIMESTAMP";
|
||||||
|
if (type.startsWith("date")) return "CURRENT_DATE";
|
||||||
|
if (type.startsWith("json")) return "'{}'";
|
||||||
|
if (type.startsWith("enum")) return "''";
|
||||||
|
return "''";
|
||||||
|
}
|
||||||
|
|
||||||
// ── Schema 版本化迁移 ───────────────────────────────────────────────────────
|
// ── Schema 版本化迁移 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -496,6 +552,7 @@ public class SystemUpdateService {
|
|||||||
+ " proxy_set_header Host $host;\n"
|
+ " proxy_set_header Host $host;\n"
|
||||||
+ " proxy_set_header X-Real-IP $remote_addr;\n"
|
+ " proxy_set_header X-Real-IP $remote_addr;\n"
|
||||||
+ " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n"
|
+ " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n"
|
||||||
|
+ " proxy_buffering off;\n"
|
||||||
+ " proxy_read_timeout 600s;\n"
|
+ " proxy_read_timeout 600s;\n"
|
||||||
+ " proxy_send_timeout 600s;\n"
|
+ " proxy_send_timeout 600s;\n"
|
||||||
+ " }\n\n"
|
+ " }\n\n"
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户