fix: remove @NotBlank from Java records, add manual validation + reset with data preservation
- Remove @Valid/@NotBlank/@Size/@Email/@NotNull from all Java record DTOs (incompatible with Jackson deserialization in Spring Boot 3.x) - Add manual validation in controllers instead - Add database reset with data preservation to reset container feature (exports core config tables, drops all tables, Hibernate recreates on startup, then restores preserved data) - Update nginx timeout regex to cover all system endpoints Affected services: tenant-service, license-service, im-service, push-service Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
这个提交包含在:
父节点
8e131906d8
当前提交
67da05dadc
@ -5,7 +5,6 @@ import com.xuqm.common.model.ApiResponse;
|
|||||||
import com.xuqm.common.security.LicenseFileCrypto;
|
import com.xuqm.common.security.LicenseFileCrypto;
|
||||||
import com.xuqm.im.service.ImAccountService;
|
import com.xuqm.im.service.ImAccountService;
|
||||||
import com.xuqm.im.service.ImAppSecretClient;
|
import com.xuqm.im.service.ImAppSecretClient;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@ -29,10 +28,13 @@ public class AuthController {
|
|||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<ApiResponse<Map<String, Object>>> login(
|
public ResponseEntity<ApiResponse<Map<String, Object>>> login(
|
||||||
@RequestParam(required = false) String appKey,
|
@RequestParam(required = false) String appKey,
|
||||||
@RequestParam @NotBlank String userId,
|
@RequestParam String userId,
|
||||||
@RequestParam @NotBlank String userSig,
|
@RequestParam String userSig,
|
||||||
@RequestParam @NotBlank String packageName,
|
@RequestParam String packageName,
|
||||||
@RequestParam(required = false) String licenseFile) {
|
@RequestParam(required = false) String licenseFile) {
|
||||||
|
if (userId == null || userId.isBlank()) throw new BusinessException(400, "userId 不能为空");
|
||||||
|
if (userSig == null || userSig.isBlank()) throw new BusinessException(400, "userSig 不能为空");
|
||||||
|
if (packageName == null || packageName.isBlank()) throw new BusinessException(400, "packageName 不能为空");
|
||||||
String resolvedAppKey = resolveAndValidate(appKey, packageName, licenseFile);
|
String resolvedAppKey = resolveAndValidate(appKey, packageName, licenseFile);
|
||||||
ImAccountService.LoginResult result = accountService.loginWithUserSig(resolvedAppKey, userId, userSig);
|
ImAccountService.LoginResult result = accountService.loginWithUserSig(resolvedAppKey, userId, userSig);
|
||||||
return ResponseEntity.ok(ApiResponse.success(Map.of("token", result.token(), "admin", result.admin())));
|
return ResponseEntity.ok(ApiResponse.success(Map.of("token", result.token(), "admin", result.admin())));
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
package com.xuqm.im.controller;
|
package com.xuqm.im.controller;
|
||||||
|
|
||||||
|
import com.xuqm.common.exception.BusinessException;
|
||||||
import com.xuqm.common.model.ApiResponse;
|
import com.xuqm.common.model.ApiResponse;
|
||||||
import com.xuqm.im.entity.ImMessageEntity;
|
import com.xuqm.im.entity.ImMessageEntity;
|
||||||
import com.xuqm.im.model.EditMessageRequest;
|
import com.xuqm.im.model.EditMessageRequest;
|
||||||
import com.xuqm.im.model.SendMessageRequest;
|
import com.xuqm.im.model.SendMessageRequest;
|
||||||
import com.xuqm.im.service.MessageService;
|
import com.xuqm.im.service.MessageService;
|
||||||
import com.xuqm.im.service.OfflineMessageSyncService;
|
import com.xuqm.im.service.OfflineMessageSyncService;
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
@ -43,9 +43,13 @@ public class MessageController {
|
|||||||
|
|
||||||
@PostMapping("/send")
|
@PostMapping("/send")
|
||||||
public ResponseEntity<ApiResponse<ImMessageEntity>> send(
|
public ResponseEntity<ApiResponse<ImMessageEntity>> send(
|
||||||
@Valid @RequestBody SendMessageRequest req,
|
@RequestBody SendMessageRequest req,
|
||||||
@AuthenticationPrincipal String userId,
|
@AuthenticationPrincipal String userId,
|
||||||
@RequestParam String appKey) {
|
@RequestParam String appKey) {
|
||||||
|
requireNonBlank(req.toId(), "toId");
|
||||||
|
requireNonNull(req.chatType(), "chatType");
|
||||||
|
requireNonNull(req.msgType(), "msgType");
|
||||||
|
requireNonBlank(req.content(), "content");
|
||||||
return ResponseEntity.ok(ApiResponse.success(messageService.send(appKey, userId, req)));
|
return ResponseEntity.ok(ApiResponse.success(messageService.send(appKey, userId, req)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,12 +64,25 @@ public class MessageController {
|
|||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public ResponseEntity<ApiResponse<ImMessageEntity>> edit(
|
public ResponseEntity<ApiResponse<ImMessageEntity>> edit(
|
||||||
@PathVariable String id,
|
@PathVariable String id,
|
||||||
@Valid @RequestBody EditMessageRequest req,
|
@RequestBody EditMessageRequest req,
|
||||||
@AuthenticationPrincipal String userId,
|
@AuthenticationPrincipal String userId,
|
||||||
@RequestParam String appKey) {
|
@RequestParam String appKey) {
|
||||||
|
requireNonBlank(req.content(), "content");
|
||||||
return ResponseEntity.ok(ApiResponse.success(messageService.edit(appKey, id, userId, req)));
|
return ResponseEntity.ok(ApiResponse.success(messageService.edit(appKey, id, userId, req)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void requireNonBlank(String value, String field) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new BusinessException(400, field + " 不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireNonNull(Object value, String field) {
|
||||||
|
if (value == null) {
|
||||||
|
throw new BusinessException(400, field + " 不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/history/{toId}")
|
@GetMapping("/history/{toId}")
|
||||||
public ResponseEntity<ApiResponse<?>> history(
|
public ResponseEntity<ApiResponse<?>> history(
|
||||||
@PathVariable String toId,
|
@PathVariable String toId,
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
package com.xuqm.im.model;
|
package com.xuqm.im.model;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
|
|
||||||
public record EditMessageRequest(
|
public record EditMessageRequest(
|
||||||
@NotBlank String content
|
String content
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
package com.xuqm.im.model;
|
package com.xuqm.im.model;
|
||||||
|
|
||||||
import com.xuqm.im.entity.ImMessageEntity;
|
import com.xuqm.im.entity.ImMessageEntity;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
public record SendMessageRequest(
|
public record SendMessageRequest(
|
||||||
String messageId,
|
String messageId,
|
||||||
@NotBlank String toId,
|
String toId,
|
||||||
@NotNull ImMessageEntity.ChatType chatType,
|
ImMessageEntity.ChatType chatType,
|
||||||
@NotNull ImMessageEntity.MsgType msgType,
|
ImMessageEntity.MsgType msgType,
|
||||||
@NotBlank String content,
|
String content,
|
||||||
String mentionedUserIds
|
String mentionedUserIds
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
package com.xuqm.push.controller;
|
package com.xuqm.push.controller;
|
||||||
|
|
||||||
|
import com.xuqm.common.exception.BusinessException;
|
||||||
import com.xuqm.common.model.ApiResponse;
|
import com.xuqm.common.model.ApiResponse;
|
||||||
import com.xuqm.push.entity.DeviceLoginLogEntity;
|
import com.xuqm.push.entity.DeviceLoginLogEntity;
|
||||||
import com.xuqm.push.service.PushAccountService;
|
import com.xuqm.push.service.PushAccountService;
|
||||||
import com.xuqm.push.service.PushDiagnosticsService;
|
import com.xuqm.push.service.PushDiagnosticsService;
|
||||||
import com.xuqm.push.service.PushDispatcher;
|
import com.xuqm.push.service.PushDispatcher;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -46,6 +46,9 @@ public class InternalPushController {
|
|||||||
if (token == null || !internalToken.equals(token)) {
|
if (token == null || !internalToken.equals(token)) {
|
||||||
return ResponseEntity.status(403).body(ApiResponse.error(403, "Forbidden"));
|
return ResponseEntity.status(403).body(ApiResponse.error(403, "Forbidden"));
|
||||||
}
|
}
|
||||||
|
requireNonBlank(request.appKey(), "appKey");
|
||||||
|
requireNonBlank(request.title(), "title");
|
||||||
|
requireNonBlank(request.body(), "body");
|
||||||
pushDispatcher.pushToUsers(request.appKey(), request.userIds(), request.title(), request.body(), request.payload());
|
pushDispatcher.pushToUsers(request.appKey(), request.userIds(), request.title(), request.body(), request.payload());
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
@ -86,6 +89,10 @@ public class InternalPushController {
|
|||||||
if (!isAllowed(token)) {
|
if (!isAllowed(token)) {
|
||||||
return ResponseEntity.status(403).body(ApiResponse.error(403, "Forbidden"));
|
return ResponseEntity.status(403).body(ApiResponse.error(403, "Forbidden"));
|
||||||
}
|
}
|
||||||
|
requireNonBlank(request.appKey(), "appKey");
|
||||||
|
requireNonBlank(request.userId(), "userId");
|
||||||
|
requireNonBlank(request.title(), "title");
|
||||||
|
requireNonBlank(request.body(), "body");
|
||||||
PushDiagnosticsService.TestPushResult result = diagnosticsService.sendTestOfflineMessage(
|
PushDiagnosticsService.TestPushResult result = diagnosticsService.sendTestOfflineMessage(
|
||||||
request.appKey(),
|
request.appKey(),
|
||||||
request.userId(),
|
request.userId(),
|
||||||
@ -130,18 +137,24 @@ public class InternalPushController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public record NotifyRequest(
|
public record NotifyRequest(
|
||||||
@NotBlank String appKey,
|
String appKey,
|
||||||
List<@NotBlank String> userIds,
|
List<String> userIds,
|
||||||
@NotBlank String title,
|
String title,
|
||||||
@NotBlank String body,
|
String body,
|
||||||
String payload
|
String payload
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public record TestOfflineRequest(
|
public record TestOfflineRequest(
|
||||||
@NotBlank String appKey,
|
String appKey,
|
||||||
@NotBlank String userId,
|
String userId,
|
||||||
@NotBlank String title,
|
String title,
|
||||||
@NotBlank String body,
|
String body,
|
||||||
String payload
|
String payload
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private static void requireNonBlank(String value, String field) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new BusinessException(400, field + " 不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
package com.xuqm.push.controller;
|
package com.xuqm.push.controller;
|
||||||
|
|
||||||
|
import com.xuqm.common.exception.BusinessException;
|
||||||
import com.xuqm.common.model.ApiResponse;
|
import com.xuqm.common.model.ApiResponse;
|
||||||
import com.xuqm.push.entity.DeviceTokenEntity;
|
import com.xuqm.push.entity.DeviceTokenEntity;
|
||||||
import com.xuqm.push.service.PushDispatcher;
|
import com.xuqm.push.service.PushDispatcher;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -24,48 +23,73 @@ public class PushController {
|
|||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<ApiResponse<Void>> register(
|
public ResponseEntity<ApiResponse<Void>> register(
|
||||||
@RequestParam @NotBlank String appKey,
|
@RequestParam String appKey,
|
||||||
@RequestParam @NotBlank String userId,
|
@RequestParam String userId,
|
||||||
@RequestParam @NotNull DeviceTokenEntity.Vendor vendor,
|
@RequestParam DeviceTokenEntity.Vendor vendor,
|
||||||
@RequestParam @NotBlank String token,
|
@RequestParam String token,
|
||||||
@RequestParam(required = false) String platform,
|
@RequestParam(required = false) String platform,
|
||||||
@RequestParam(required = false) String deviceId,
|
@RequestParam(required = false) String deviceId,
|
||||||
@RequestParam(required = false) String brand,
|
@RequestParam(required = false) String brand,
|
||||||
@RequestParam(required = false) String model,
|
@RequestParam(required = false) String model,
|
||||||
@RequestParam(required = false) String osVersion,
|
@RequestParam(required = false) String osVersion,
|
||||||
@RequestParam(required = false) String appVersion) {
|
@RequestParam(required = false) String appVersion) {
|
||||||
|
requireNonBlank(appKey, "appKey");
|
||||||
|
requireNonBlank(userId, "userId");
|
||||||
|
requireNonNull(vendor, "vendor");
|
||||||
|
requireNonBlank(token, "token");
|
||||||
pushDispatcher.registerToken(appKey, userId, vendor, token, platform, deviceId, brand, model, osVersion, appVersion);
|
pushDispatcher.registerToken(appKey, userId, vendor, token, platform, deviceId, brand, model, osVersion, appVersion);
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/receive-push")
|
@PostMapping("/receive-push")
|
||||||
public ResponseEntity<ApiResponse<Void>> receivePush(
|
public ResponseEntity<ApiResponse<Void>> receivePush(
|
||||||
@RequestParam @NotBlank String appKey,
|
@RequestParam String appKey,
|
||||||
@RequestParam @NotBlank String userId,
|
@RequestParam String userId,
|
||||||
@RequestParam(required = false) String deviceId,
|
@RequestParam(required = false) String deviceId,
|
||||||
@RequestParam boolean enabled) {
|
@RequestParam boolean enabled) {
|
||||||
|
requireNonBlank(appKey, "appKey");
|
||||||
|
requireNonBlank(userId, "userId");
|
||||||
pushDispatcher.setReceivePush(appKey, userId, deviceId, enabled);
|
pushDispatcher.setReceivePush(appKey, userId, deviceId, enabled);
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/send")
|
@PostMapping("/send")
|
||||||
public ResponseEntity<ApiResponse<Void>> send(
|
public ResponseEntity<ApiResponse<Void>> send(
|
||||||
@RequestParam @NotBlank String appKey,
|
@RequestParam String appKey,
|
||||||
@RequestParam @NotBlank String userId,
|
@RequestParam String userId,
|
||||||
@RequestParam @NotBlank String title,
|
@RequestParam String title,
|
||||||
@RequestParam @NotBlank String body,
|
@RequestParam String body,
|
||||||
@RequestParam(required = false) String payload) {
|
@RequestParam(required = false) String payload) {
|
||||||
|
requireNonBlank(appKey, "appKey");
|
||||||
|
requireNonBlank(userId, "userId");
|
||||||
|
requireNonBlank(title, "title");
|
||||||
|
requireNonBlank(body, "body");
|
||||||
pushDispatcher.pushToUser(appKey, userId, title, body, payload);
|
pushDispatcher.pushToUser(appKey, userId, title, body, payload);
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/unregister")
|
@DeleteMapping("/unregister")
|
||||||
public ResponseEntity<ApiResponse<Void>> unregister(
|
public ResponseEntity<ApiResponse<Void>> unregister(
|
||||||
@RequestParam @NotBlank String appKey,
|
@RequestParam String appKey,
|
||||||
@RequestParam @NotBlank String userId,
|
@RequestParam String userId,
|
||||||
@RequestParam @NotNull DeviceTokenEntity.Vendor vendor,
|
@RequestParam DeviceTokenEntity.Vendor vendor,
|
||||||
@RequestParam(required = false) String deviceId) {
|
@RequestParam(required = false) String deviceId) {
|
||||||
|
requireNonBlank(appKey, "appKey");
|
||||||
|
requireNonBlank(userId, "userId");
|
||||||
|
requireNonNull(vendor, "vendor");
|
||||||
pushDispatcher.unregisterToken(appKey, userId, vendor, deviceId);
|
pushDispatcher.unregisterToken(appKey, userId, vendor, deviceId);
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void requireNonBlank(String value, String field) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new BusinessException(400, field + " 不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireNonNull(Object value, String field) {
|
||||||
|
if (value == null) {
|
||||||
|
throw new BusinessException(400, field + " 不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import com.xuqm.tenant.service.AppUserClient;
|
|||||||
import com.xuqm.tenant.service.EmailService;
|
import com.xuqm.tenant.service.EmailService;
|
||||||
import com.xuqm.tenant.service.FeatureServiceManager;
|
import com.xuqm.tenant.service.FeatureServiceManager;
|
||||||
import com.xuqm.tenant.service.OperationLogService;
|
import com.xuqm.tenant.service.OperationLogService;
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import org.springframework.http.ContentDisposition;
|
import org.springframework.http.ContentDisposition;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@ -68,18 +67,28 @@ public class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<ApiResponse<AppEntity>> create(@Valid @RequestBody CreateAppRequest req,
|
public ResponseEntity<ApiResponse<AppEntity>> create(@RequestBody CreateAppRequest req,
|
||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
|
requireNonBlank(req.packageName(), "packageName");
|
||||||
|
requireNonBlank(req.name(), "name");
|
||||||
return ResponseEntity.ok(ApiResponse.success(appService.create(tenantId, req)));
|
return ResponseEntity.ok(ApiResponse.success(appService.create(tenantId, req)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{appKey}")
|
@PutMapping("/{appKey}")
|
||||||
public ResponseEntity<ApiResponse<AppEntity>> update(@PathVariable String appKey,
|
public ResponseEntity<ApiResponse<AppEntity>> update(@PathVariable String appKey,
|
||||||
@Valid @RequestBody CreateAppRequest req,
|
@RequestBody CreateAppRequest req,
|
||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
|
requireNonBlank(req.packageName(), "packageName");
|
||||||
|
requireNonBlank(req.name(), "name");
|
||||||
return ResponseEntity.ok(ApiResponse.success(appService.update(appKey, tenantId, req)));
|
return ResponseEntity.ok(ApiResponse.success(appService.update(appKey, tenantId, req)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void requireNonBlank(String value, String field) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new BusinessException(400, field + " 不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{appKey}")
|
@DeleteMapping("/{appKey}")
|
||||||
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable String appKey,
|
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable String appKey,
|
||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
package com.xuqm.tenant.controller;
|
package com.xuqm.tenant.controller;
|
||||||
|
|
||||||
|
import com.xuqm.common.exception.BusinessException;
|
||||||
import com.xuqm.common.model.ApiResponse;
|
import com.xuqm.common.model.ApiResponse;
|
||||||
import com.xuqm.tenant.dto.LoginRequest;
|
import com.xuqm.tenant.dto.LoginRequest;
|
||||||
import com.xuqm.tenant.dto.RegisterRequest;
|
import com.xuqm.tenant.dto.RegisterRequest;
|
||||||
import com.xuqm.tenant.service.AuthService;
|
import com.xuqm.tenant.service.AuthService;
|
||||||
import com.xuqm.tenant.service.EmailService;
|
import com.xuqm.tenant.service.EmailService;
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.validation.constraints.Email;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@ -79,36 +77,57 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/send-email-code")
|
@PostMapping("/send-email-code")
|
||||||
public ResponseEntity<ApiResponse<Void>> sendEmailCode(@RequestParam @NotBlank @Email String email,
|
public ResponseEntity<ApiResponse<Void>> sendEmailCode(@RequestParam String email,
|
||||||
@RequestParam @NotBlank String purpose) {
|
@RequestParam String purpose) {
|
||||||
|
requireNonBlank(email, "email");
|
||||||
|
requireNonBlank(purpose, "purpose");
|
||||||
emailService.sendVerificationCode(email, purpose);
|
emailService.sendVerificationCode(email, purpose);
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<ApiResponse<Void>> register(@Valid @RequestBody RegisterRequest req) {
|
public ResponseEntity<ApiResponse<Void>> register(@RequestBody RegisterRequest req) {
|
||||||
|
requireNonBlank(req.username(), "username");
|
||||||
|
requireNonBlank(req.password(), "password");
|
||||||
|
requireNonBlank(req.email(), "email");
|
||||||
|
requireNonBlank(req.nickname(), "nickname");
|
||||||
|
requireNonBlank(req.emailCode(), "emailCode");
|
||||||
authService.register(req);
|
authService.register(req);
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<ApiResponse<Map<String, String>>> login(@Valid @RequestBody LoginRequest req) {
|
public ResponseEntity<ApiResponse<Map<String, String>>> login(@RequestBody LoginRequest req) {
|
||||||
|
requireNonBlank(req.account(), "account");
|
||||||
|
requireNonBlank(req.password(), "password");
|
||||||
|
requireNonBlank(req.captchaKey(), "captchaKey");
|
||||||
|
requireNonBlank(req.captchaCode(), "captchaCode");
|
||||||
String token = authService.login(req);
|
String token = authService.login(req);
|
||||||
return ResponseEntity.ok(ApiResponse.success(Map.of("token", token)));
|
return ResponseEntity.ok(ApiResponse.success(Map.of("token", token)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/forgot-password")
|
@PostMapping("/forgot-password")
|
||||||
public ResponseEntity<ApiResponse<Void>> forgotPassword(@RequestParam @NotBlank @Email String email) {
|
public ResponseEntity<ApiResponse<Void>> forgotPassword(@RequestParam String email) {
|
||||||
|
requireNonBlank(email, "email");
|
||||||
authService.forgotPassword(email);
|
authService.forgotPassword(email);
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/reset-password")
|
@PostMapping("/reset-password")
|
||||||
public ResponseEntity<ApiResponse<Void>> resetPassword(
|
public ResponseEntity<ApiResponse<Void>> resetPassword(
|
||||||
@RequestParam @NotBlank @Email String email,
|
@RequestParam String email,
|
||||||
@RequestParam @NotBlank String code,
|
@RequestParam String code,
|
||||||
@RequestParam @NotBlank String newPassword) {
|
@RequestParam String newPassword) {
|
||||||
|
requireNonBlank(email, "email");
|
||||||
|
requireNonBlank(code, "code");
|
||||||
|
requireNonBlank(newPassword, "newPassword");
|
||||||
authService.resetPassword(email, code, newPassword);
|
authService.resetPassword(email, code, newPassword);
|
||||||
return ResponseEntity.ok(ApiResponse.ok());
|
return ResponseEntity.ok(ApiResponse.ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void requireNonBlank(String value, String field) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new BusinessException(400, field + " 不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,9 +7,6 @@ import com.xuqm.tenant.entity.TenantEntity;
|
|||||||
import com.xuqm.tenant.service.EmailService;
|
import com.xuqm.tenant.service.EmailService;
|
||||||
import com.xuqm.tenant.service.OperationLogService;
|
import com.xuqm.tenant.service.OperationLogService;
|
||||||
import com.xuqm.tenant.service.SubAccountService;
|
import com.xuqm.tenant.service.SubAccountService;
|
||||||
import jakarta.validation.Valid;
|
|
||||||
import jakarta.validation.constraints.Email;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
@ -45,8 +42,9 @@ public class SubAccountController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/send-verify-code")
|
@PostMapping("/send-verify-code")
|
||||||
public ResponseEntity<ApiResponse<Void>> sendVerifyCode(@RequestParam @NotBlank @Email String email,
|
public ResponseEntity<ApiResponse<Void>> sendVerifyCode(@RequestParam String email,
|
||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
|
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", Map.of(
|
||||||
"email", email
|
"email", email
|
||||||
@ -55,9 +53,11 @@ public class SubAccountController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/verify-email")
|
@PostMapping("/verify-email")
|
||||||
public ResponseEntity<ApiResponse<Void>> verifyEmail(@RequestParam @NotBlank @Email String email,
|
public ResponseEntity<ApiResponse<Void>> verifyEmail(@RequestParam String email,
|
||||||
@RequestParam @NotBlank String code,
|
@RequestParam String code,
|
||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
|
requireNonBlank(email, "email");
|
||||||
|
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", Map.of(
|
||||||
"email", email
|
"email", email
|
||||||
@ -66,14 +66,23 @@ public class SubAccountController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<ApiResponse<TenantEntity>> create(@Valid @RequestBody CreateSubAccountRequest req,
|
public ResponseEntity<ApiResponse<TenantEntity>> create(@RequestBody CreateSubAccountRequest req,
|
||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
|
requireNonBlank(req.username(), "username");
|
||||||
|
requireNonBlank(req.password(), "password");
|
||||||
|
requireNonBlank(req.nickname(), "nickname");
|
||||||
if (!subAccountService.isEmailVerifiedInSession(tenantId)) {
|
if (!subAccountService.isEmailVerifiedInSession(tenantId)) {
|
||||||
throw new BusinessException(403, "请先完成邮箱验证");
|
throw new BusinessException(403, "请先完成邮箱验证");
|
||||||
}
|
}
|
||||||
return ResponseEntity.ok(ApiResponse.success(subAccountService.create(tenantId, req)));
|
return ResponseEntity.ok(ApiResponse.success(subAccountService.create(tenantId, req)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void requireNonBlank(String value, String field) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new BusinessException(400, field + " 不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public ResponseEntity<ApiResponse<Void>> disable(@PathVariable String id,
|
public ResponseEntity<ApiResponse<Void>> disable(@PathVariable String id,
|
||||||
@AuthenticationPrincipal String tenantId) {
|
@AuthenticationPrincipal String tenantId) {
|
||||||
|
|||||||
@ -80,7 +80,8 @@ public class SystemUpdateController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 不拉取新镜像,直接用当前本地镜像重建所有容器。速度快,适合修复异常服务。
|
* 保留数据,重置容器和数据库表结构。
|
||||||
|
* 流程:备份核心数据 → 删表 → 重建容器 → 恢复数据 → 执行迁移。
|
||||||
* 仅 PRIVATE 模式可用。
|
* 仅 PRIVATE 模式可用。
|
||||||
*/
|
*/
|
||||||
@PostMapping(value = "/reset", produces = MediaType.TEXT_PLAIN_VALUE)
|
@PostMapping(value = "/reset", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||||
|
|||||||
@ -1,13 +1,10 @@
|
|||||||
package com.xuqm.tenant.dto;
|
package com.xuqm.tenant.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.Size;
|
|
||||||
|
|
||||||
public record CreateAppRequest(
|
public record CreateAppRequest(
|
||||||
@NotBlank @Size(max = 128) String packageName,
|
String packageName,
|
||||||
@Size(max = 128) String iosBundleId,
|
String iosBundleId,
|
||||||
@Size(max = 128) String harmonyBundleName,
|
String harmonyBundleName,
|
||||||
@NotBlank @Size(max = 128) String name,
|
String name,
|
||||||
@Size(max = 512) String description,
|
String description,
|
||||||
String iconUrl
|
String iconUrl
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@ -1,12 +1,9 @@
|
|||||||
package com.xuqm.tenant.dto;
|
package com.xuqm.tenant.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.Size;
|
|
||||||
|
|
||||||
public record CreateSubAccountRequest(
|
public record CreateSubAccountRequest(
|
||||||
@NotBlank @Size(min = 3, max = 32) String username,
|
String username,
|
||||||
@NotBlank @Size(min = 6, max = 64) String password,
|
String password,
|
||||||
String email,
|
String email,
|
||||||
@NotBlank @Size(max = 32) String nickname,
|
String nickname,
|
||||||
String phone
|
String phone
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
package com.xuqm.tenant.dto;
|
package com.xuqm.tenant.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
|
|
||||||
public record LoginRequest(
|
public record LoginRequest(
|
||||||
@NotBlank String account,
|
String account,
|
||||||
@NotBlank String password,
|
String password,
|
||||||
@NotBlank String captchaKey,
|
String captchaKey,
|
||||||
@NotBlank String captchaCode
|
String captchaCode
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
package com.xuqm.tenant.dto;
|
package com.xuqm.tenant.dto;
|
||||||
|
|
||||||
import jakarta.validation.constraints.Email;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.Size;
|
|
||||||
|
|
||||||
public record RegisterRequest(
|
public record RegisterRequest(
|
||||||
@NotBlank @Size(min = 3, max = 32) String username,
|
String username,
|
||||||
@NotBlank @Size(min = 6, max = 64) String password,
|
String password,
|
||||||
@NotBlank @Email String email,
|
String email,
|
||||||
@NotBlank @Size(max = 32) String nickname,
|
String nickname,
|
||||||
String phone,
|
String phone,
|
||||||
@NotBlank String emailCode
|
String emailCode
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@ -21,7 +21,9 @@ import java.sql.PreparedStatement;
|
|||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.Statement;
|
import java.sql.Statement;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -50,13 +52,6 @@ public class SystemUpdateService {
|
|||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 启动时自动执行迁移 ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
|
||||||
public void onApplicationReady() {
|
|
||||||
runSchemaMigrations(line -> log.info("[migration] {}", line));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 公开接口 ────────────────────────────────────────────────────────────────
|
// ── 公开接口 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -158,15 +153,184 @@ public class SystemUpdateService {
|
|||||||
restartAndSelfUpdate(emit, composeFile);
|
restartAndSelfUpdate(emit, composeFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 不拉取新镜像,直接用当前本地镜像重建所有容器。 */
|
/** 保留数据,重置容器和数据库表结构。 */
|
||||||
public void runReset(Consumer<String> emit) {
|
public void runReset(Consumer<String> emit) {
|
||||||
String composeFile = deployRoot + "/docker-compose.yml";
|
String composeFile = deployRoot + "/docker-compose.yml";
|
||||||
|
|
||||||
patchConfigs(emit);
|
patchConfigs(emit);
|
||||||
runSchemaMigrations(emit);
|
resetDatabaseSchema(emit);
|
||||||
restartAndSelfUpdate(emit, composeFile);
|
restartAndSelfUpdate(emit, composeFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 数据库重置(保留核心数据)──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要保留数据的核心表及其主键列。
|
||||||
|
* 导出 → 删表 → 重启后 Hibernate 重建 → 恢复数据。
|
||||||
|
*/
|
||||||
|
private static final Map<String, String> PRESERVE_TABLES = new java.util.LinkedHashMap<>();
|
||||||
|
static {
|
||||||
|
PRESERVE_TABLES.put("t_tenant", "id");
|
||||||
|
PRESERVE_TABLES.put("t_ops_admin", "id");
|
||||||
|
PRESERVE_TABLES.put("t_app", "id");
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetDatabaseSchema(Consumer<String> emit) {
|
||||||
|
emit.accept(">>> 重置数据库表结构(保留核心数据)...");
|
||||||
|
try (Connection conn = dataSource.getConnection();
|
||||||
|
Statement stmt = conn.createStatement()) {
|
||||||
|
|
||||||
|
// 1. 导出核心表数据到临时表
|
||||||
|
Map<String, String> backupTables = new java.util.LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, String> entry : PRESERVE_TABLES.entrySet()) {
|
||||||
|
String table = entry.getKey();
|
||||||
|
String tmpTable = "_backup_" + table;
|
||||||
|
try {
|
||||||
|
// 检查源表是否存在
|
||||||
|
try (ResultSet rs = stmt.executeQuery(
|
||||||
|
"SELECT COUNT(*) FROM information_schema.TABLES " +
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" + table + "'")) {
|
||||||
|
if (!rs.next() || rs.getInt(1) == 0) continue;
|
||||||
|
}
|
||||||
|
stmt.execute("DROP TABLE IF EXISTS `" + tmpTable + "`");
|
||||||
|
stmt.execute("CREATE TABLE `" + tmpTable + "` AS SELECT * FROM `" + table + "`");
|
||||||
|
long count = 0;
|
||||||
|
try (ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM `" + tmpTable + "`")) {
|
||||||
|
if (rs.next()) count = rs.getLong(1);
|
||||||
|
}
|
||||||
|
backupTables.put(table, tmpTable);
|
||||||
|
emit.accept(" 已备份 " + table + " (" + count + " 行)");
|
||||||
|
} catch (Exception e) {
|
||||||
|
emit.accept(" [警告] 备份 " + table + " 失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 禁用外键检查,删除所有业务表
|
||||||
|
stmt.execute("SET FOREIGN_KEY_CHECKS = 0");
|
||||||
|
try (ResultSet rs = stmt.executeQuery(
|
||||||
|
"SELECT TABLE_NAME FROM information_schema.TABLES " +
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'")) {
|
||||||
|
List<String> tables = new java.util.ArrayList<>();
|
||||||
|
while (rs.next()) {
|
||||||
|
String name = rs.getString("TABLE_NAME");
|
||||||
|
if (!"_schema_migrations".equals(name) && !name.startsWith("_backup_")) {
|
||||||
|
tables.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String table : tables) {
|
||||||
|
stmt.execute("DROP TABLE IF EXISTS `" + table + "`");
|
||||||
|
}
|
||||||
|
emit.accept(" 已删除 " + tables.size() + " 张业务表");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 清空迁移记录,保留备份表
|
||||||
|
stmt.execute("DELETE FROM _schema_migrations");
|
||||||
|
stmt.execute("SET FOREIGN_KEY_CHECKS = 1");
|
||||||
|
|
||||||
|
// 4. 写入恢复脚本,供启动后执行
|
||||||
|
saveRestoreScript(backupTables);
|
||||||
|
emit.accept(">>> 数据库表结构已重置,容器重启后将自动重建并恢复数据");
|
||||||
|
} catch (Exception e) {
|
||||||
|
emit.accept(" [错误] 重置数据库失败: " + e.getMessage());
|
||||||
|
log.error("reset database schema failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将恢复指令写入文件,供 onApplicationReady 读取执行。
|
||||||
|
* 格式:每行 "源表名:备份表名:主键列"
|
||||||
|
*/
|
||||||
|
private void saveRestoreScript(Map<String, String> backupTables) {
|
||||||
|
try {
|
||||||
|
Path script = Paths.get(deployRoot, ".db-restore-pending");
|
||||||
|
List<String> lines = backupTables.entrySet().stream()
|
||||||
|
.map(e -> e.getKey() + ":" + e.getValue() + ":" + PRESERVE_TABLES.get(e.getKey()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Files.write(script, lines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||||
|
log.info("wrote restore script: {}", script);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("failed to write restore script", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动时检查是否有待恢复的数据。
|
||||||
|
* 在 Hibernate 建表之后、迁移之前执行。
|
||||||
|
*/
|
||||||
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
public void onApplicationReady() {
|
||||||
|
restoreFromBackup();
|
||||||
|
runSchemaMigrations(line -> log.info("[migration] {}", line));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restoreFromBackup() {
|
||||||
|
Path script = Paths.get(deployRoot, ".db-restore-pending");
|
||||||
|
if (!Files.exists(script)) return;
|
||||||
|
|
||||||
|
log.info("restoring data from backup tables...");
|
||||||
|
try (Connection conn = dataSource.getConnection();
|
||||||
|
Statement stmt = conn.createStatement()) {
|
||||||
|
List<String> lines = Files.readAllLines(script);
|
||||||
|
for (String line : lines) {
|
||||||
|
String[] parts = line.split(":");
|
||||||
|
if (parts.length != 3) continue;
|
||||||
|
String table = parts[0];
|
||||||
|
String tmpTable = parts[1];
|
||||||
|
String pk = parts[2];
|
||||||
|
try {
|
||||||
|
// 检查备份表是否存在
|
||||||
|
try (ResultSet rs = stmt.executeQuery(
|
||||||
|
"SELECT COUNT(*) FROM information_schema.TABLES " +
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" + tmpTable + "'")) {
|
||||||
|
if (!rs.next() || rs.getInt(1) == 0) {
|
||||||
|
log.info("backup table {} not found, skip", tmpTable);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 获取列列表(取目标表和备份表的交集)
|
||||||
|
List<String> columns = getCommonColumns(stmt, table, tmpTable);
|
||||||
|
if (columns.isEmpty()) {
|
||||||
|
log.warn("no common columns between {} and {}", table, tmpTable);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String colList = columns.stream().map(c -> "`" + c + "`").collect(Collectors.joining(", "));
|
||||||
|
String sql = "INSERT IGNORE INTO `" + table + "` (" + colList + ") " +
|
||||||
|
"SELECT " + colList + " FROM `" + tmpTable + "`";
|
||||||
|
int rows = stmt.executeUpdate(sql);
|
||||||
|
log.info("restored {} rows into {}", rows, table);
|
||||||
|
|
||||||
|
// 删除备份表
|
||||||
|
stmt.execute("DROP TABLE IF EXISTS `" + tmpTable + "`");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("restore {} failed: {}", table, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Files.deleteIfExists(script);
|
||||||
|
log.info("data restore complete");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("restore from backup failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getCommonColumns(Statement stmt, String table, String tmpTable) throws Exception {
|
||||||
|
Set<String> tableCols = new java.util.LinkedHashSet<>();
|
||||||
|
try (ResultSet rs = stmt.executeQuery("SHOW COLUMNS FROM `" + table + "`")) {
|
||||||
|
while (rs.next()) tableCols.add(rs.getString("Field"));
|
||||||
|
}
|
||||||
|
List<String> common = new java.util.ArrayList<>();
|
||||||
|
try (ResultSet rs = stmt.executeQuery("SHOW COLUMNS FROM `" + tmpTable + "`")) {
|
||||||
|
while (rs.next()) {
|
||||||
|
String col = rs.getString("Field");
|
||||||
|
if (tableCols.contains(col)) common.add(col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return common;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Schema 版本化迁移 ───────────────────────────────────────────────────────
|
// ── Schema 版本化迁移 ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户