diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/AppController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/AppController.java index 2cb36ba..4278387 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/AppController.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/AppController.java @@ -106,9 +106,10 @@ public class AppController { TenantEntity tenant = tenantRepository.findById(tenantId) .orElseThrow(() -> new RuntimeException("Tenant not found")); emailService.sendVerificationCode(tenant.getEmail(), purpose); - operationLogService.record(tenantId, "APP", "APP_SECRET", appKey, "REQUEST_SECRET_VERIFY", Map.of( - "purpose", purpose - )); + String purposeLabel = "REVEAL_SECRET".equals(purpose) ? "查看密钥" : "重置密钥"; + operationLogService.record(tenantId, "APP", "APP_SECRET", appKey, "REQUEST_SECRET_VERIFY", + "申请" + purposeLabel + "验证码,应用 " + appKey, + Map.of("purpose", purpose)); return ResponseEntity.ok(ApiResponse.ok()); } @@ -122,9 +123,9 @@ public class AppController { TenantEntity tenant = tenantRepository.findById(tenantId) .orElseThrow(() -> new RuntimeException("Tenant not found")); emailService.verify(tenant.getEmail(), body.get("code"), "REVEAL_SECRET"); - operationLogService.record(tenantId, "APP", "APP_SECRET", appKey, "REVEAL_APP_SECRET", Map.of( - "appKey", app.getAppKey() - )); + operationLogService.record(tenantId, "APP", "APP_SECRET", appKey, "REVEAL_APP_SECRET", + "查看应用「" + app.getName() + "」的 AppSecret", + Map.of("appKey", app.getAppKey())); return ResponseEntity.ok(ApiResponse.success(Map.of("appSecret", app.getAppSecret()))); } diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/FeatureServiceController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/FeatureServiceController.java index 3e9a8c4..2842266 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/FeatureServiceController.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/FeatureServiceController.java @@ -65,10 +65,9 @@ public class FeatureServiceController { throw new com.xuqm.common.exception.BusinessException(400, "开启服务请通过 request-activation 申请"); } FeatureServiceEntity saved = featureServiceManager.disable(appKey, platform, serviceType); - operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", saved.getId(), "DISABLE_SERVICE", java.util.Map.of( - "platform", platform.name(), - "serviceType", serviceType.name() - )); + operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", saved.getId(), "DISABLE_SERVICE", + "停用 " + platform.name() + " 平台的 " + serviceType.name() + " 服务", + java.util.Map.of("platform", platform.name(), "serviceType", serviceType.name())); return ResponseEntity.ok(ApiResponse.success(saved)); } @@ -118,10 +117,9 @@ public class FeatureServiceController { }; FeatureServiceEntity saved = featureServiceManager.updateConfig( appKey, platform, serviceType, config); - operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", saved.getId(), "UPDATE_SERVICE_CONFIG", java.util.Map.of( - "platform", platform.name(), - "serviceType", serviceType.name() - )); + operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", saved.getId(), "UPDATE_SERVICE_CONFIG", + "更新 " + platform.name() + " 平台 " + serviceType.name() + " 服务配置", + java.util.Map.of("platform", platform.name(), "serviceType", serviceType.name())); return ResponseEntity.ok(ApiResponse.success(saved)); } @@ -141,7 +139,9 @@ public class FeatureServiceController { if (applyReason != null && !applyReason.isBlank()) { 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)); } @@ -159,10 +159,9 @@ public class FeatureServiceController { @AuthenticationPrincipal String tenantId) { appService.getByAppKey(appKey, tenantId); FeatureServiceEntity updated = featureServiceManager.regenerateSecretKey(id); - operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", updated.getId(), "REGENERATE_KEY", java.util.Map.of( - "platform", updated.getPlatform().name(), - "serviceType", updated.getServiceType().name() - )); + operationLogService.record(tenantId, "SERVICE", "FEATURE_SERVICE", updated.getId(), "REGENERATE_KEY", + "重新生成 " + updated.getPlatform().name() + " 平台 " + updated.getServiceType().name() + " 服务密钥", + java.util.Map.of("platform", updated.getPlatform().name(), "serviceType", updated.getServiceType().name())); return ResponseEntity.ok(ApiResponse.success(updated)); } diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/SubAccountController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/SubAccountController.java index b564fb9..30ffc2e 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/SubAccountController.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/SubAccountController.java @@ -46,9 +46,9 @@ public class SubAccountController { @AuthenticationPrincipal String tenantId) { requireNonBlank(email, "email"); emailService.sendVerificationCode(email, "SUB_ACCOUNT"); - operationLogService.record(tenantId, "SUB_ACCOUNT", "EMAIL_VERIFY", email, "SEND_VERIFY_CODE", Map.of( - "email", email - )); + operationLogService.record(tenantId, "SUB_ACCOUNT", "EMAIL_VERIFY", email, "SEND_VERIFY_CODE", + "发送子账号邮箱验证码到 " + email, + Map.of("email", email)); return ResponseEntity.ok(ApiResponse.ok()); } @@ -59,9 +59,9 @@ public class SubAccountController { requireNonBlank(email, "email"); requireNonBlank(code, "code"); subAccountService.verifyEmail(tenantId, email, code); - operationLogService.record(tenantId, "SUB_ACCOUNT", "EMAIL_VERIFY", email, "VERIFY_EMAIL", Map.of( - "email", email - )); + operationLogService.record(tenantId, "SUB_ACCOUNT", "EMAIL_VERIFY", email, "VERIFY_EMAIL", + "验证子账号邮箱 " + email + " 成功", + Map.of("email", email)); return ResponseEntity.ok(ApiResponse.ok()); } diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/SystemUpdateController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/SystemUpdateController.java index 16ef29b..9c4df74 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/SystemUpdateController.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/SystemUpdateController.java @@ -103,6 +103,8 @@ public class SystemUpdateController { }); return ResponseEntity.ok() .contentType(MediaType.TEXT_PLAIN) + .header("X-Accel-Buffering", "no") + .header("Cache-Control", "no-cache, no-store") .body(body); } } diff --git a/tenant-service/src/main/java/com/xuqm/tenant/entity/OperationLogEntity.java b/tenant-service/src/main/java/com/xuqm/tenant/entity/OperationLogEntity.java index 833b9a2..c3c6bfe 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/entity/OperationLogEntity.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/entity/OperationLogEntity.java @@ -39,6 +39,12 @@ public class OperationLogEntity { @Column(columnDefinition = "TEXT") private String detailJson; + @Column(length = 255) + private String summary; + + @Column(length = 64) + private String ipAddress; + @Column(nullable = false) private LocalDateTime createdAt; @@ -66,6 +72,12 @@ public class OperationLogEntity { public String getDetailJson() { return 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 void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } } diff --git a/tenant-service/src/main/java/com/xuqm/tenant/service/AppService.java b/tenant-service/src/main/java/com/xuqm/tenant/service/AppService.java index b1fa780..6d7ac3a 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/service/AppService.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/service/AppService.java @@ -82,11 +82,9 @@ public class AppService { app.setLicenseFileContent(generateLicenseFileContent(app)); AppEntity saved = appRepository.save(app); autoEnableFileService(saved.getAppKey()); - operationLogService.record(tenantId, "APP", "APP", saved.getAppKey(), "CREATE_APP", Map.of( - "name", saved.getName(), - "packageName", saved.getPackageName(), - "appKey", saved.getAppKey() - )); + operationLogService.record(tenantId, "APP", "APP", saved.getAppKey(), "CREATE_APP", + "创建应用「" + saved.getName() + "」(" + saved.getPackageName() + ")", + Map.of("name", saved.getName(), "packageName", saved.getPackageName(), "appKey", saved.getAppKey())); return saved; } @@ -121,21 +119,18 @@ public class AppService { after.put("packageName", saved.getPackageName()); after.put("description", saved.getDescription()); after.put("iconUrl", saved.getIconUrl()); - operationLogService.record(tenantId, "APP", "APP", saved.getAppKey(), "UPDATE_APP", Map.of( - "before", before, - "after", after - )); + operationLogService.record(tenantId, "APP", "APP", saved.getAppKey(), "UPDATE_APP", + "更新应用「" + saved.getName() + "」的信息", + Map.of("before", before, "after", after)); return saved; } public void delete(String appKey, String tenantId) { AppEntity app = getByAppKey(appKey, tenantId); appRepository.delete(app); - operationLogService.record(tenantId, "APP", "APP", app.getAppKey(), "DELETE_APP", Map.of( - "name", app.getName(), - "packageName", app.getPackageName(), - "appKey", app.getAppKey() - )); + operationLogService.record(tenantId, "APP", "APP", app.getAppKey(), "DELETE_APP", + "删除应用「" + app.getName() + "」(" + app.getPackageName() + ")", + Map.of("name", app.getName(), "packageName", app.getPackageName(), "appKey", app.getAppKey())); } public String resetSecret(String appKey, String tenantId) { @@ -143,11 +138,9 @@ public class AppService { String newSecret = generateSecret(); app.setAppSecret(newSecret); appRepository.save(app); - operationLogService.record(tenantId, "APP", "APP_SECRET", app.getAppKey(), "RESET_APP_SECRET", Map.of( - "name", app.getName(), - "packageName", app.getPackageName(), - "appKey", app.getAppKey() - )); + operationLogService.record(tenantId, "APP", "APP_SECRET", app.getAppKey(), "RESET_APP_SECRET", + "重置应用「" + app.getName() + "」的 AppSecret", + Map.of("name", app.getName(), "packageName", app.getPackageName(), "appKey", app.getAppKey())); return newSecret; } diff --git a/tenant-service/src/main/java/com/xuqm/tenant/service/DashboardService.java b/tenant-service/src/main/java/com/xuqm/tenant/service/DashboardService.java index 27f764a..83881ae 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/service/DashboardService.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/service/DashboardService.java @@ -44,7 +44,8 @@ public class DashboardService { result.put("serviceCount", serviceCount); result.put("subAccountCount", subAccountCount); - operationLogService.record(tenantId, "CONSOLE", "DASHBOARD", tenantId, "VIEW_DASHBOARD", result); + operationLogService.record(tenantId, "CONSOLE", "DASHBOARD", tenantId, "VIEW_DASHBOARD", + "查看控制台概览", result); return result; } } diff --git a/tenant-service/src/main/java/com/xuqm/tenant/service/OperationLogService.java b/tenant-service/src/main/java/com/xuqm/tenant/service/OperationLogService.java index 16f60cf..788b49f 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/service/OperationLogService.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/service/OperationLogService.java @@ -3,11 +3,14 @@ package com.xuqm.tenant.service; import com.fasterxml.jackson.databind.ObjectMapper; import com.xuqm.tenant.entity.OperationLogEntity; import com.xuqm.tenant.repository.OperationLogRepository; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; 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.util.LinkedHashMap; @@ -31,6 +34,16 @@ public class OperationLogService { String resourceId, String action, Map 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 detail) { OperationLogEntity entity = new OperationLogEntity(); entity.setId(UUID.randomUUID().toString()); entity.setTenantId(tenantId); @@ -39,6 +52,8 @@ public class OperationLogService { entity.setResourceId(resourceId); entity.setAction(action); entity.setOperator(currentOperator()); + entity.setSummary(summary); + entity.setIpAddress(resolveClientIp()); entity.setDetailJson(serialize(detail)); entity.setCreatedAt(LocalDateTime.now()); repository.save(entity); @@ -65,6 +80,27 @@ public class OperationLogService { 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 detail) { try { Map payload = detail == null ? Map.of() : new LinkedHashMap<>(detail); diff --git a/tenant-service/src/main/java/com/xuqm/tenant/service/SubAccountService.java b/tenant-service/src/main/java/com/xuqm/tenant/service/SubAccountService.java index 55e6947..a42cf70 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/service/SubAccountService.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/service/SubAccountService.java @@ -64,11 +64,9 @@ public class SubAccountService { sub.setParentId(parentId); sub.setCreatedAt(LocalDateTime.now()); TenantEntity saved = tenantRepository.save(sub); - operationLogService.record(parentId, "SUB_ACCOUNT", "SUB_ACCOUNT", saved.getId(), "CREATE_SUB_ACCOUNT", Map.of( - "username", saved.getUsername(), - "nickname", saved.getNickname(), - "email", saved.getEmail() - )); + operationLogService.record(parentId, "SUB_ACCOUNT", "SUB_ACCOUNT", saved.getId(), "CREATE_SUB_ACCOUNT", + "创建子账号「" + saved.getNickname() + "」(" + saved.getUsername() + ")", + Map.of("username", saved.getUsername(), "nickname", saved.getNickname(), "email", saved.getEmail())); return saved; } @@ -84,11 +82,9 @@ public class SubAccountService { } sub.setStatus(TenantEntity.Status.DISABLED); tenantRepository.save(sub); - operationLogService.record(parentId, "SUB_ACCOUNT", "SUB_ACCOUNT", sub.getId(), "DISABLE_SUB_ACCOUNT", Map.of( - "username", sub.getUsername(), - "nickname", sub.getNickname(), - "email", sub.getEmail() - )); + operationLogService.record(parentId, "SUB_ACCOUNT", "SUB_ACCOUNT", sub.getId(), "DISABLE_SUB_ACCOUNT", + "禁用子账号「" + sub.getNickname() + "」(" + sub.getUsername() + ")", + Map.of("username", sub.getUsername(), "nickname", sub.getNickname(), "email", sub.getEmail())); } public String generatePassword() { diff --git a/tenant-service/src/main/java/com/xuqm/tenant/service/SystemUpdateService.java b/tenant-service/src/main/java/com/xuqm/tenant/service/SystemUpdateService.java index e900231..2e126bd 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/service/SystemUpdateService.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/service/SystemUpdateService.java @@ -176,7 +176,11 @@ public class SystemUpdateService { PRESERVE_TABLES.put("t_feature_service", "id"); PRESERVE_TABLES.put("t_risk_config", "id"); 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 emit) { @@ -297,9 +301,23 @@ public class SystemUpdateService { log.warn("no common columns between {} and {}", table, tmpTable); continue; } + // 获取目标表列的 NOT NULL 和 DEFAULT 信息 + Map> colMeta = getColumnMeta(stmt, table); + // 对 NOT NULL 列用 COALESCE 兜底,避免 INSERT IGNORE 静默丢行 + List selectExprs = new java.util.ArrayList<>(); + for (String col : columns) { + Map 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 sql = "INSERT IGNORE INTO `" + table + "` (" + colList + ") " + - "SELECT " + colList + " FROM `" + tmpTable + "`"; + String selectList = String.join(", ", selectExprs); + String sql = "INSERT INTO `" + table + "` (" + colList + ") " + + "SELECT " + selectList + " FROM `" + tmpTable + "`"; int rows = stmt.executeUpdate(sql); log.info("restored {} rows into {}", rows, table); @@ -331,6 +349,44 @@ public class SystemUpdateService { return common; } + /** 获取目标表各列的 Null、Default 等元信息。 */ + private Map> getColumnMeta(Statement stmt, String table) throws Exception { + Map> meta = new java.util.LinkedHashMap<>(); + try (ResultSet rs = stmt.executeQuery("SHOW COLUMNS FROM `" + table + "`")) { + while (rs.next()) { + Map 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 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 版本化迁移 ─────────────────────────────────────────────────────── /** @@ -496,6 +552,7 @@ public class SystemUpdateService { + " proxy_set_header Host $host;\n" + " proxy_set_header X-Real-IP $remote_addr;\n" + " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n" + + " proxy_buffering off;\n" + " proxy_read_timeout 600s;\n" + " proxy_send_timeout 600s;\n" + " }\n\n"