This commit is contained in:
2026-08-03 13:16:19 +08:00
parent 481747d9dc
commit 9eb03d4bce
54 changed files with 1925 additions and 52 deletions
@@ -56,6 +56,11 @@ public class InternalAccountController {
@PathVariable Long id, @Valid @RequestBody ResetPasswordRequest request) {
verify(token); service.resetPassword(id, request); return ApiResponse.success(null);
}
@PutMapping("/{id}/scope")
public ApiResponse<Void> scope(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
@PathVariable Long id, @Valid @RequestBody UpdateAccountScopeRequest request) {
verify(token); service.updateScope(id, request); return ApiResponse.success(null);
}
@DeleteMapping("/{id}/compensation")
public ApiResponse<Void> compensate(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
@PathVariable Long id) {
@@ -5,6 +5,8 @@ import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.Pattern;
import java.time.LocalDateTime;
/** 账号安全及内部账号管理契约。 */
public final class AccountManagementDtos {
private AccountManagementDtos() { }
@@ -19,10 +21,24 @@ public final class AccountManagementDtos {
@NotBlank String displayName, @NotNull Long tenantId,
@NotNull Long membershipId,
@Pattern(regexp = "MERCHANT|PLATFORM", message = "应用端只能是 MERCHANT 或 PLATFORM")
String appCode) { }
String appCode,
@Pattern(regexp = "ACTIVE|DISABLED", message = "账号状态只能是 ACTIVE 或 DISABLED")
String accountStatus) {
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
Long tenantId, Long membershipId, String appCode) {
this(accountId, username, password, displayName, tenantId, membershipId, appCode, "ACTIVE");
}
}
public record UpdateAccountProfileRequest(@NotBlank @Size(max = 64) String displayName) { }
public record UpdateAccountStatusRequest(@NotBlank String status) { }
public record ResetPasswordRequest(@NotBlank @Size(min = 8, max = 64)
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String newPassword) { }
public record AccountSummary(Long accountId, String username, String displayName, String status) { }
public record UpdateAccountScopeRequest(@NotNull Long tenantId, @NotNull Long membershipId,
@NotBlank @Pattern(regexp = "MERCHANT|PLATFORM") String appCode) { }
public record AccountSummary(Long accountId, String username, String displayName, String status,
LocalDateTime lastLoginAt, Long tenantId, Long membershipId, String appCode) {
public AccountSummary(Long accountId, String username, String displayName, String status) {
this(accountId, username, displayName, status, null, null, null, null);
}
}
}
@@ -54,7 +54,7 @@ public class AccountManagementService {
account.setUsername(request.username());
account.setPasswordHash(passwordEncoder.encode(request.password()));
account.setDisplayName(request.displayName());
account.setAccountStatus("ACTIVE");
account.setAccountStatus(request.accountStatus() == null ? "ACTIVE" : request.accountStatus());
account.setFailedLoginCount(0);
account.setTokenVersion(1);
account.setDefaultTenantId(request.tenantId());
@@ -93,6 +93,20 @@ public class AccountManagementService {
setPasswordAndRevoke(required(accountId), request.newPassword());
}
@Transactional
@BusinessAudit(event = "ACCOUNT_SCOPE_CHANGE", resourceType = "ACCOUNT", resourceId = "#accountId",
description = "修改账号所属租户", captureAfter = false)
public void updateScope(Long accountId, UpdateAccountScopeRequest request) {
String appCode = request.appCode().trim().toUpperCase(Locale.ROOT);
validateScope(appCode, request.tenantId());
AuthAccount account = required(accountId);
account.setDefaultTenantId(request.tenantId());
account.setOperatorTenantId(request.tenantId());
account.setDefaultMembershipId(request.membershipId());
account.setDefaultAppCode(appCode);
bumpVersion(account);
}
@Transactional
@BusinessAudit(event = "ACCOUNT_COMPENSATE_DELETE", resourceType = "ACCOUNT",
resourceId = "#accountId", description = "补偿删除认证账号", captureAfter = false)
@@ -127,7 +141,8 @@ public class AccountManagementService {
}
private AccountSummary summary(AuthAccount a) {
return new AccountSummary(a.getId(), a.getUsername(), a.getDisplayName(), a.getAccountStatus());
return new AccountSummary(a.getId(), a.getUsername(), a.getDisplayName(), a.getAccountStatus(),
a.getLastLoginAt(), a.getDefaultTenantId(), a.getDefaultMembershipId(), a.getDefaultAppCode());
}
private void validateScope(String appCode, Long tenantId) {
@@ -4,6 +4,7 @@ import com.baiyue.auth.dto.AccountManagementDtos.ChangePasswordRequest;
import com.baiyue.auth.dto.AccountManagementDtos.CreateAccountRequest;
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountProfileRequest;
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountStatusRequest;
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountScopeRequest;
import com.baiyue.auth.entity.AuthAccount;
import com.baiyue.auth.repository.AuthAccountRepository;
import com.baiyue.common.core.exception.BizException;
@@ -15,6 +16,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.time.LocalDateTime;
@DisplayName("账号管理服务")
class AccountManagementServiceTest {
@@ -69,6 +71,37 @@ class AccountManagementServiceTest {
assertNotEquals("Password1", captor.getValue().getPasswordHash());
}
@Test void createDisabledAccountAndSummaryExposeLastLoginAndScope() {
when(repository.findById(20L)).thenReturn(null);
when(repository.existsByUsername("merchant.user")).thenReturn(false);
var created = service.create(new CreateAccountRequest(20L, "merchant.user", "Password1",
"企业用户", 10001L, 30L, "MERCHANT", "DISABLED"));
var captor = org.mockito.ArgumentCaptor.forClass(AuthAccount.class);
verify(repository).insert(captor.capture());
AuthAccount stored = captor.getValue();
stored.setLastLoginAt(LocalDateTime.of(2026, 6, 1, 10, 30));
when(repository.findById(20L)).thenReturn(stored);
var summary = service.summary(20L);
assertEquals("DISABLED", created.status());
assertEquals(10001L, summary.tenantId());
assertEquals(30L, summary.membershipId());
assertEquals(LocalDateTime.of(2026, 6, 1, 10, 30), summary.lastLoginAt());
}
@Test void updateScopeChangesTenantAndRevokesExistingSessions() {
account.setDefaultTenantId(10001L); account.setOperatorTenantId(10001L);
account.setDefaultMembershipId(30L); account.setDefaultAppCode("MERCHANT");
service.updateScope(10L, new UpdateAccountScopeRequest(10002L, 30L, "MERCHANT"));
assertEquals(10002L, account.getDefaultTenantId());
assertEquals(10002L, account.getOperatorTenantId());
verify(repository).save(account);
verify(sessions).revokeAll(10L);
}
@Test void createAccountRetriesIdempotentlyForSameAccountAndOwner() {
account.setUsername("platform.user"); account.setDisplayName("平台用户");
account.setDefaultTenantId(0L); account.setOperatorTenantId(0L);