暂存提交

This commit is contained in:
2026-07-31 13:52:35 +08:00
parent 11e740706d
commit bc3c18a6e9
35 changed files with 421 additions and 121 deletions
@@ -6,6 +6,7 @@ import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
@@ -17,13 +18,14 @@ import java.security.MessageDigest;
@RestController
@RequestMapping("/internal/auth/accounts")
public class InternalAccountController {
private final AccountManagementService service;
@Resource
private AccountManagementService service;
private final String configuredToken;
private byte[] tokenBytes;
public InternalAccountController(AccountManagementService service,
public InternalAccountController(
@Value("${baiyue.security.internal-service-token}") String configuredToken) {
this.service = service;
this.configuredToken = configuredToken;
}
@@ -44,6 +46,11 @@ public class InternalAccountController {
@PathVariable Long id, @Valid @RequestBody UpdateAccountStatusRequest request) {
verify(token); service.updateStatus(id, request); return ApiResponse.success(null);
}
@PutMapping("/{id}/profile")
public ApiResponse<Void> profile(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
@PathVariable Long id, @Valid @RequestBody UpdateAccountProfileRequest request) {
verify(token); service.updateProfile(id, request); return ApiResponse.success(null);
}
@PutMapping("/{id}/password")
public ApiResponse<Void> reset(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
@PathVariable Long id, @Valid @RequestBody ResetPasswordRequest request) {
@@ -17,7 +17,10 @@ public final class AccountManagementDtos {
@NotBlank @Size(min = 8, max = 64)
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
@NotBlank String displayName, @NotNull Long tenantId,
@NotNull Long membershipId) { }
@NotNull Long membershipId,
@Pattern(regexp = "MERCHANT|PLATFORM", message = "应用端只能是 MERCHANT 或 PLATFORM")
String appCode) { }
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) { }
@@ -11,6 +11,9 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baiyue.common.audit.BusinessAudit;
import java.util.Locale;
import java.util.Objects;
/** 账号生命周期管理,所有密码只以 BCrypt 摘要写入数据库。 */
@Service
public class AccountManagementService {
@@ -35,7 +38,15 @@ public class AccountManagementService {
@BusinessAudit(event = "ACCOUNT_CREATE", resourceType = "ACCOUNT",
resourceId = "#request.accountId", description = "创建认证账号")
public AccountSummary create(CreateAccountRequest request) {
if (repository.existsByUsername(request.username()) || repository.findById(request.accountId()) != null) {
String appCode = request.appCode() == null ? "MERCHANT"
: request.appCode().trim().toUpperCase(Locale.ROOT);
validateScope(appCode, request.tenantId());
AuthAccount existing = repository.findById(request.accountId());
if (existing != null) {
if (sameIdentity(existing, request, appCode)) return summary(existing);
throw new BizException(ResultCode.CONFLICT, "认证账号标识已被其他成员使用");
}
if (repository.existsByUsername(request.username())) {
throw new BizException(ResultCode.CONFLICT, "登录账号已存在");
}
AuthAccount account = new AuthAccount();
@@ -49,7 +60,7 @@ public class AccountManagementService {
account.setDefaultTenantId(request.tenantId());
account.setOperatorTenantId(request.tenantId());
account.setDefaultMembershipId(request.membershipId());
account.setDefaultAppCode("MERCHANT");
account.setDefaultAppCode(appCode);
account.setDeleted(0);
repository.insert(account);
return summary(account);
@@ -66,6 +77,16 @@ public class AccountManagementService {
bumpVersion(account);
}
@Transactional
@BusinessAudit(event = "ACCOUNT_PROFILE_CHANGE", resourceType = "ACCOUNT", resourceId = "#accountId",
description = "同步账号显示名称", captureAfter = false)
public void updateProfile(Long accountId, UpdateAccountProfileRequest request) {
AuthAccount account = required(accountId);
if (Objects.equals(account.getDisplayName(), request.displayName())) return;
account.setDisplayName(request.displayName());
bumpVersion(account);
}
@Transactional
@BusinessAudit(event = "PASSWORD_RESET", resourceType = "ACCOUNT", resourceId = "#accountId", description = "管理员重置密码", captureAfter = false)
public void resetPassword(Long accountId, ResetPasswordRequest request) {
@@ -108,4 +129,23 @@ public class AccountManagementService {
private AccountSummary summary(AuthAccount a) {
return new AccountSummary(a.getId(), a.getUsername(), a.getDisplayName(), a.getAccountStatus());
}
private void validateScope(String appCode, Long tenantId) {
if (!"MERCHANT".equals(appCode) && !"PLATFORM".equals(appCode)) {
throw new BizException(ResultCode.BAD_REQUEST, "应用端只能是 MERCHANT 或 PLATFORM");
}
if (("PLATFORM".equals(appCode) && tenantId != 0L)
|| ("MERCHANT".equals(appCode) && tenantId <= 0L)) {
throw new BizException(ResultCode.BAD_REQUEST, "账号应用端与租户范围不匹配");
}
}
private boolean sameIdentity(AuthAccount account, CreateAccountRequest request, String appCode) {
return Objects.equals(account.getUsername(), request.username())
&& Objects.equals(account.getDefaultTenantId(), request.tenantId())
&& Objects.equals(account.getOperatorTenantId(), request.tenantId())
&& Objects.equals(account.getDefaultMembershipId(), request.membershipId())
&& Objects.equals(account.getDefaultAppCode(), appCode)
&& !Integer.valueOf(1).equals(account.getDeleted());
}
}
@@ -1,6 +1,8 @@
package com.baiyue.auth.service;
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.entity.AuthAccount;
import com.baiyue.auth.repository.AuthAccountRepository;
@@ -49,4 +51,41 @@ class AccountManagementServiceTest {
assertEquals("DISABLED", account.getAccountStatus());
verify(sessions).revokeAll(10L);
}
@Test void 创建平台账号保存平台应用域且密码仅保存摘要() {
when(repository.findById(20L)).thenReturn(null);
when(repository.existsByUsername("platform.user")).thenReturn(false);
var result = service.create(new CreateAccountRequest(20L, "platform.user", "Password1",
"平台用户", 0L, 30L, "PLATFORM"));
assertEquals("platform.user", result.username());
var captor = org.mockito.ArgumentCaptor.forClass(AuthAccount.class);
verify(repository).insert(captor.capture());
assertEquals("PLATFORM", captor.getValue().getDefaultAppCode());
assertTrue(encoder.matches("Password1", captor.getValue().getPasswordHash()));
assertNotEquals("Password1", captor.getValue().getPasswordHash());
}
@Test void 相同账号标识和归属重试时幂等返回() {
account.setUsername("platform.user"); account.setDisplayName("平台用户");
account.setDefaultTenantId(0L); account.setOperatorTenantId(0L);
account.setDefaultMembershipId(30L); account.setDefaultAppCode("PLATFORM"); account.setDeleted(0);
var result = service.create(new CreateAccountRequest(10L, "platform.user", "Password1",
"平台用户", 0L, 30L, "PLATFORM"));
assertEquals(10L, result.accountId());
verify(repository, never()).insert(any());
}
@Test void 同步显示名撤销已有会话() {
account.setDisplayName("旧名称");
service.updateProfile(10L, new UpdateAccountProfileRequest("新名称"));
assertEquals("新名称", account.getDisplayName());
verify(repository).save(account);
verify(sessions).revokeAll(10L);
}
}