diff --git a/im-service/src/main/java/com/xuqm/im/controller/AuthController.java b/im-service/src/main/java/com/xuqm/im/controller/AuthController.java index 1a16857..7ff038a 100644 --- a/im-service/src/main/java/com/xuqm/im/controller/AuthController.java +++ b/im-service/src/main/java/com/xuqm/im/controller/AuthController.java @@ -5,7 +5,6 @@ import com.xuqm.common.model.ApiResponse; import com.xuqm.common.security.LicenseFileCrypto; import com.xuqm.im.service.ImAccountService; import com.xuqm.im.service.ImAppSecretClient; -import jakarta.validation.constraints.NotBlank; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -29,10 +28,13 @@ public class AuthController { @PostMapping("/login") public ResponseEntity>> login( @RequestParam(required = false) String appKey, - @RequestParam @NotBlank String userId, - @RequestParam @NotBlank String userSig, - @RequestParam @NotBlank String packageName, + @RequestParam String userId, + @RequestParam String userSig, + @RequestParam String packageName, @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); ImAccountService.LoginResult result = accountService.loginWithUserSig(resolvedAppKey, userId, userSig); return ResponseEntity.ok(ApiResponse.success(Map.of("token", result.token(), "admin", result.admin()))); diff --git a/im-service/src/main/java/com/xuqm/im/controller/MessageController.java b/im-service/src/main/java/com/xuqm/im/controller/MessageController.java index 4375e2f..4eae80a 100644 --- a/im-service/src/main/java/com/xuqm/im/controller/MessageController.java +++ b/im-service/src/main/java/com/xuqm/im/controller/MessageController.java @@ -1,12 +1,12 @@ package com.xuqm.im.controller; +import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; import com.xuqm.im.entity.ImMessageEntity; import com.xuqm.im.model.EditMessageRequest; import com.xuqm.im.model.SendMessageRequest; import com.xuqm.im.service.MessageService; import com.xuqm.im.service.OfflineMessageSyncService; -import jakarta.validation.Valid; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -43,9 +43,13 @@ public class MessageController { @PostMapping("/send") public ResponseEntity> send( - @Valid @RequestBody SendMessageRequest req, + @RequestBody SendMessageRequest req, @AuthenticationPrincipal String userId, @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))); } @@ -60,12 +64,25 @@ public class MessageController { @PutMapping("/{id}") public ResponseEntity> edit( @PathVariable String id, - @Valid @RequestBody EditMessageRequest req, + @RequestBody EditMessageRequest req, @AuthenticationPrincipal String userId, @RequestParam String appKey) { + requireNonBlank(req.content(), "content"); 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}") public ResponseEntity> history( @PathVariable String toId, diff --git a/im-service/src/main/java/com/xuqm/im/model/EditMessageRequest.java b/im-service/src/main/java/com/xuqm/im/model/EditMessageRequest.java index 75f044c..f073e5e 100644 --- a/im-service/src/main/java/com/xuqm/im/model/EditMessageRequest.java +++ b/im-service/src/main/java/com/xuqm/im/model/EditMessageRequest.java @@ -1,7 +1,5 @@ package com.xuqm.im.model; -import jakarta.validation.constraints.NotBlank; - public record EditMessageRequest( - @NotBlank String content + String content ) {} diff --git a/im-service/src/main/java/com/xuqm/im/model/SendMessageRequest.java b/im-service/src/main/java/com/xuqm/im/model/SendMessageRequest.java index ad2a999..572556c 100644 --- a/im-service/src/main/java/com/xuqm/im/model/SendMessageRequest.java +++ b/im-service/src/main/java/com/xuqm/im/model/SendMessageRequest.java @@ -1,14 +1,12 @@ package com.xuqm.im.model; import com.xuqm.im.entity.ImMessageEntity; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; public record SendMessageRequest( String messageId, - @NotBlank String toId, - @NotNull ImMessageEntity.ChatType chatType, - @NotNull ImMessageEntity.MsgType msgType, - @NotBlank String content, + String toId, + ImMessageEntity.ChatType chatType, + ImMessageEntity.MsgType msgType, + String content, String mentionedUserIds ) {} diff --git a/push-service/src/main/java/com/xuqm/push/controller/InternalPushController.java b/push-service/src/main/java/com/xuqm/push/controller/InternalPushController.java index f635f4c..2bb360f 100644 --- a/push-service/src/main/java/com/xuqm/push/controller/InternalPushController.java +++ b/push-service/src/main/java/com/xuqm/push/controller/InternalPushController.java @@ -1,11 +1,11 @@ package com.xuqm.push.controller; +import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; import com.xuqm.push.entity.DeviceLoginLogEntity; import com.xuqm.push.service.PushAccountService; import com.xuqm.push.service.PushDiagnosticsService; import com.xuqm.push.service.PushDispatcher; -import jakarta.validation.constraints.NotBlank; import org.springframework.data.domain.Page; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; @@ -46,6 +46,9 @@ public class InternalPushController { if (token == null || !internalToken.equals(token)) { 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()); return ResponseEntity.ok(ApiResponse.ok()); } @@ -86,6 +89,10 @@ public class InternalPushController { if (!isAllowed(token)) { 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( request.appKey(), request.userId(), @@ -130,18 +137,24 @@ public class InternalPushController { } public record NotifyRequest( - @NotBlank String appKey, - List<@NotBlank String> userIds, - @NotBlank String title, - @NotBlank String body, + String appKey, + List userIds, + String title, + String body, String payload ) {} public record TestOfflineRequest( - @NotBlank String appKey, - @NotBlank String userId, - @NotBlank String title, - @NotBlank String body, + String appKey, + String userId, + String title, + String body, String payload ) {} + + private static void requireNonBlank(String value, String field) { + if (value == null || value.isBlank()) { + throw new BusinessException(400, field + " 不能为空"); + } + } } diff --git a/push-service/src/main/java/com/xuqm/push/controller/PushController.java b/push-service/src/main/java/com/xuqm/push/controller/PushController.java index 2b69c88..5897a35 100644 --- a/push-service/src/main/java/com/xuqm/push/controller/PushController.java +++ b/push-service/src/main/java/com/xuqm/push/controller/PushController.java @@ -1,10 +1,9 @@ package com.xuqm.push.controller; +import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; import com.xuqm.push.entity.DeviceTokenEntity; import com.xuqm.push.service.PushDispatcher; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -24,48 +23,73 @@ public class PushController { @PostMapping("/register") public ResponseEntity> register( - @RequestParam @NotBlank String appKey, - @RequestParam @NotBlank String userId, - @RequestParam @NotNull DeviceTokenEntity.Vendor vendor, - @RequestParam @NotBlank String token, + @RequestParam String appKey, + @RequestParam String userId, + @RequestParam DeviceTokenEntity.Vendor vendor, + @RequestParam String token, @RequestParam(required = false) String platform, @RequestParam(required = false) String deviceId, @RequestParam(required = false) String brand, @RequestParam(required = false) String model, @RequestParam(required = false) String osVersion, @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); return ResponseEntity.ok(ApiResponse.ok()); } @PostMapping("/receive-push") public ResponseEntity> receivePush( - @RequestParam @NotBlank String appKey, - @RequestParam @NotBlank String userId, + @RequestParam String appKey, + @RequestParam String userId, @RequestParam(required = false) String deviceId, @RequestParam boolean enabled) { + requireNonBlank(appKey, "appKey"); + requireNonBlank(userId, "userId"); pushDispatcher.setReceivePush(appKey, userId, deviceId, enabled); return ResponseEntity.ok(ApiResponse.ok()); } @PostMapping("/send") public ResponseEntity> send( - @RequestParam @NotBlank String appKey, - @RequestParam @NotBlank String userId, - @RequestParam @NotBlank String title, - @RequestParam @NotBlank String body, + @RequestParam String appKey, + @RequestParam String userId, + @RequestParam String title, + @RequestParam String body, @RequestParam(required = false) String payload) { + requireNonBlank(appKey, "appKey"); + requireNonBlank(userId, "userId"); + requireNonBlank(title, "title"); + requireNonBlank(body, "body"); pushDispatcher.pushToUser(appKey, userId, title, body, payload); return ResponseEntity.ok(ApiResponse.ok()); } @DeleteMapping("/unregister") public ResponseEntity> unregister( - @RequestParam @NotBlank String appKey, - @RequestParam @NotBlank String userId, - @RequestParam @NotNull DeviceTokenEntity.Vendor vendor, + @RequestParam String appKey, + @RequestParam String userId, + @RequestParam DeviceTokenEntity.Vendor vendor, @RequestParam(required = false) String deviceId) { + requireNonBlank(appKey, "appKey"); + requireNonBlank(userId, "userId"); + requireNonNull(vendor, "vendor"); pushDispatcher.unregisterToken(appKey, userId, vendor, deviceId); 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 + " 不能为空"); + } + } } 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 fbbbdf9..2cb36ba 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 @@ -13,7 +13,6 @@ import com.xuqm.tenant.service.AppUserClient; import com.xuqm.tenant.service.EmailService; import com.xuqm.tenant.service.FeatureServiceManager; import com.xuqm.tenant.service.OperationLogService; -import jakarta.validation.Valid; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -68,18 +67,28 @@ public class AppController { } @PostMapping - public ResponseEntity> create(@Valid @RequestBody CreateAppRequest req, + public ResponseEntity> create(@RequestBody CreateAppRequest req, @AuthenticationPrincipal String tenantId) { + requireNonBlank(req.packageName(), "packageName"); + requireNonBlank(req.name(), "name"); return ResponseEntity.ok(ApiResponse.success(appService.create(tenantId, req))); } @PutMapping("/{appKey}") public ResponseEntity> update(@PathVariable String appKey, - @Valid @RequestBody CreateAppRequest req, + @RequestBody CreateAppRequest req, @AuthenticationPrincipal String tenantId) { + requireNonBlank(req.packageName(), "packageName"); + requireNonBlank(req.name(), "name"); 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}") public ResponseEntity> delete(@PathVariable String appKey, @AuthenticationPrincipal String tenantId) { diff --git a/tenant-service/src/main/java/com/xuqm/tenant/controller/AuthController.java b/tenant-service/src/main/java/com/xuqm/tenant/controller/AuthController.java index 4da36b1..198f423 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/controller/AuthController.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/controller/AuthController.java @@ -1,13 +1,11 @@ package com.xuqm.tenant.controller; +import com.xuqm.common.exception.BusinessException; import com.xuqm.common.model.ApiResponse; import com.xuqm.tenant.dto.LoginRequest; import com.xuqm.tenant.dto.RegisterRequest; import com.xuqm.tenant.service.AuthService; 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.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -79,36 +77,57 @@ public class AuthController { } @PostMapping("/send-email-code") - public ResponseEntity> sendEmailCode(@RequestParam @NotBlank @Email String email, - @RequestParam @NotBlank String purpose) { + public ResponseEntity> sendEmailCode(@RequestParam String email, + @RequestParam String purpose) { + requireNonBlank(email, "email"); + requireNonBlank(purpose, "purpose"); emailService.sendVerificationCode(email, purpose); return ResponseEntity.ok(ApiResponse.ok()); } @PostMapping("/register") - public ResponseEntity> register(@Valid @RequestBody RegisterRequest req) { + public ResponseEntity> 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); return ResponseEntity.ok(ApiResponse.ok()); } @PostMapping("/login") - public ResponseEntity>> login(@Valid @RequestBody LoginRequest req) { + public ResponseEntity>> 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); return ResponseEntity.ok(ApiResponse.success(Map.of("token", token))); } @PostMapping("/forgot-password") - public ResponseEntity> forgotPassword(@RequestParam @NotBlank @Email String email) { + public ResponseEntity> forgotPassword(@RequestParam String email) { + requireNonBlank(email, "email"); authService.forgotPassword(email); return ResponseEntity.ok(ApiResponse.ok()); } @PostMapping("/reset-password") public ResponseEntity> resetPassword( - @RequestParam @NotBlank @Email String email, - @RequestParam @NotBlank String code, - @RequestParam @NotBlank String newPassword) { + @RequestParam String email, + @RequestParam String code, + @RequestParam String newPassword) { + requireNonBlank(email, "email"); + requireNonBlank(code, "code"); + requireNonBlank(newPassword, "newPassword"); authService.resetPassword(email, code, newPassword); return ResponseEntity.ok(ApiResponse.ok()); } + + private static void requireNonBlank(String value, String field) { + if (value == null || value.isBlank()) { + throw new BusinessException(400, field + " 不能为空"); + } + } } 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 fe05c63..b564fb9 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 @@ -7,9 +7,6 @@ import com.xuqm.tenant.entity.TenantEntity; import com.xuqm.tenant.service.EmailService; import com.xuqm.tenant.service.OperationLogService; 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.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; @@ -45,8 +42,9 @@ public class SubAccountController { } @PostMapping("/send-verify-code") - public ResponseEntity> sendVerifyCode(@RequestParam @NotBlank @Email String email, + public ResponseEntity> sendVerifyCode(@RequestParam String email, @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 @@ -55,9 +53,11 @@ public class SubAccountController { } @PostMapping("/verify-email") - public ResponseEntity> verifyEmail(@RequestParam @NotBlank @Email String email, - @RequestParam @NotBlank String code, + public ResponseEntity> verifyEmail(@RequestParam String email, + @RequestParam String code, @AuthenticationPrincipal String tenantId) { + 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 @@ -66,14 +66,23 @@ public class SubAccountController { } @PostMapping - public ResponseEntity> create(@Valid @RequestBody CreateSubAccountRequest req, + public ResponseEntity> create(@RequestBody CreateSubAccountRequest req, @AuthenticationPrincipal String tenantId) { + requireNonBlank(req.username(), "username"); + requireNonBlank(req.password(), "password"); + requireNonBlank(req.nickname(), "nickname"); if (!subAccountService.isEmailVerifiedInSession(tenantId)) { throw new BusinessException(403, "请先完成邮箱验证"); } 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}") public ResponseEntity> disable(@PathVariable String id, @AuthenticationPrincipal String tenantId) { 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 0ddd926..16ef29b 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 @@ -80,7 +80,8 @@ public class SystemUpdateController { } /** - * 不拉取新镜像,直接用当前本地镜像重建所有容器。速度快,适合修复异常服务。 + * 保留数据,重置容器和数据库表结构。 + * 流程:备份核心数据 → 删表 → 重建容器 → 恢复数据 → 执行迁移。 * 仅 PRIVATE 模式可用。 */ @PostMapping(value = "/reset", produces = MediaType.TEXT_PLAIN_VALUE) diff --git a/tenant-service/src/main/java/com/xuqm/tenant/dto/CreateAppRequest.java b/tenant-service/src/main/java/com/xuqm/tenant/dto/CreateAppRequest.java index 4dc64ef..5957811 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/dto/CreateAppRequest.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/dto/CreateAppRequest.java @@ -1,13 +1,10 @@ package com.xuqm.tenant.dto; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; - public record CreateAppRequest( - @NotBlank @Size(max = 128) String packageName, - @Size(max = 128) String iosBundleId, - @Size(max = 128) String harmonyBundleName, - @NotBlank @Size(max = 128) String name, - @Size(max = 512) String description, + String packageName, + String iosBundleId, + String harmonyBundleName, + String name, + String description, String iconUrl ) {} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/dto/CreateSubAccountRequest.java b/tenant-service/src/main/java/com/xuqm/tenant/dto/CreateSubAccountRequest.java index dc248f0..d01876e 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/dto/CreateSubAccountRequest.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/dto/CreateSubAccountRequest.java @@ -1,12 +1,9 @@ package com.xuqm.tenant.dto; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; - public record CreateSubAccountRequest( - @NotBlank @Size(min = 3, max = 32) String username, - @NotBlank @Size(min = 6, max = 64) String password, + String username, + String password, String email, - @NotBlank @Size(max = 32) String nickname, + String nickname, String phone ) {} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/dto/LoginRequest.java b/tenant-service/src/main/java/com/xuqm/tenant/dto/LoginRequest.java index f74573b..65a32d1 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/dto/LoginRequest.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/dto/LoginRequest.java @@ -1,10 +1,8 @@ package com.xuqm.tenant.dto; -import jakarta.validation.constraints.NotBlank; - public record LoginRequest( - @NotBlank String account, - @NotBlank String password, - @NotBlank String captchaKey, - @NotBlank String captchaCode + String account, + String password, + String captchaKey, + String captchaCode ) {} diff --git a/tenant-service/src/main/java/com/xuqm/tenant/dto/RegisterRequest.java b/tenant-service/src/main/java/com/xuqm/tenant/dto/RegisterRequest.java index 406f279..f577584 100644 --- a/tenant-service/src/main/java/com/xuqm/tenant/dto/RegisterRequest.java +++ b/tenant-service/src/main/java/com/xuqm/tenant/dto/RegisterRequest.java @@ -1,14 +1,10 @@ package com.xuqm.tenant.dto; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; - public record RegisterRequest( - @NotBlank @Size(min = 3, max = 32) String username, - @NotBlank @Size(min = 6, max = 64) String password, - @NotBlank @Email String email, - @NotBlank @Size(max = 32) String nickname, + String username, + String password, + String email, + String nickname, String phone, - @NotBlank String emailCode + String emailCode ) {} 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 1c3a9b4..e900231 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 @@ -21,7 +21,9 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Statement; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -50,13 +52,6 @@ public class SystemUpdateService { 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); } - /** 不拉取新镜像,直接用当前本地镜像重建所有容器。 */ + /** 保留数据,重置容器和数据库表结构。 */ public void runReset(Consumer emit) { String composeFile = deployRoot + "/docker-compose.yml"; patchConfigs(emit); - runSchemaMigrations(emit); + resetDatabaseSchema(emit); restartAndSelfUpdate(emit, composeFile); } + // ── 数据库重置(保留核心数据)────────────────────────────────────────────── + + /** + * 需要保留数据的核心表及其主键列。 + * 导出 → 删表 → 重启后 Hibernate 重建 → 恢复数据。 + */ + private static final Map 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 emit) { + emit.accept(">>> 重置数据库表结构(保留核心数据)..."); + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + + // 1. 导出核心表数据到临时表 + Map backupTables = new java.util.LinkedHashMap<>(); + for (Map.Entry 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 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 backupTables) { + try { + Path script = Paths.get(deployRoot, ".db-restore-pending"); + List 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 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 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 getCommonColumns(Statement stmt, String table, String tmpTable) throws Exception { + Set tableCols = new java.util.LinkedHashSet<>(); + try (ResultSet rs = stmt.executeQuery("SHOW COLUMNS FROM `" + table + "`")) { + while (rs.next()) tableCols.add(rs.getString("Field")); + } + List 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 版本化迁移 ─────────────────────────────────────────────────────── /**