feat(auth): support username or phone login
This commit is contained in:
@@ -85,6 +85,20 @@ public class InternalAccountController {
|
|||||||
@PathVariable Long id, @Valid @RequestBody UpdateAccountProfileRequest request) {
|
@PathVariable Long id, @Valid @RequestBody UpdateAccountProfileRequest request) {
|
||||||
verify(token); service.updateProfile(id, request); return ApiResponse.success(null);
|
verify(token); service.updateProfile(id, request); return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
@PutMapping("/{id}/login-identifiers")
|
||||||
|
public ApiResponse<AccountSummary> loginIdentifiers(
|
||||||
|
@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
|
||||||
|
@PathVariable Long id, @Valid @RequestBody ChangeLoginIdentifiersRequest request) {
|
||||||
|
verify(token);
|
||||||
|
return ApiResponse.success(service.changeLoginIdentifiers(id, request));
|
||||||
|
}
|
||||||
|
@PutMapping("/{id}/phone")
|
||||||
|
public ApiResponse<Void> phone(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
|
||||||
|
@PathVariable Long id, @Valid @RequestBody UpdateAccountPhoneRequest request) {
|
||||||
|
verify(token);
|
||||||
|
service.updatePhone(id, request);
|
||||||
|
return ApiResponse.success(null);
|
||||||
|
}
|
||||||
@PutMapping("/{id}/password")
|
@PutMapping("/{id}/password")
|
||||||
public ApiResponse<Void> reset(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
|
public ApiResponse<Void> reset(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
|
||||||
@PathVariable Long id, @Valid @RequestBody ResetPasswordRequest request) {
|
@PathVariable Long id, @Valid @RequestBody ResetPasswordRequest request) {
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ public final class AccountManagementDtos {
|
|||||||
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字")
|
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字")
|
||||||
String newPassword) { }
|
String newPassword) { }
|
||||||
public record CreateAccountRequest(@NotNull Long accountId,
|
public record CreateAccountRequest(@NotNull Long accountId,
|
||||||
@NotBlank @Pattern(regexp = "^[A-Za-z0-9_.@-]{4,64}$") String username,
|
@NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
|
||||||
|
@Pattern(regexp = "^1\\d{10}$") String phone,
|
||||||
@NotBlank @Size(min = 8, max = 64)
|
@NotBlank @Size(min = 8, max = 64)
|
||||||
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
|
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
|
||||||
@NotBlank String displayName, @NotNull Long tenantId,
|
@NotBlank String displayName, @NotNull Long tenantId,
|
||||||
@@ -32,9 +33,19 @@ public final class AccountManagementDtos {
|
|||||||
String accountStatus) {
|
String accountStatus) {
|
||||||
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
||||||
Long tenantId, Long membershipId, String appCode) {
|
Long tenantId, Long membershipId, String appCode) {
|
||||||
this(accountId, username, password, displayName, tenantId, membershipId, appCode, "ACTIVE");
|
this(accountId, username, null, password, displayName, tenantId, membershipId, appCode, "ACTIVE");
|
||||||
|
}
|
||||||
|
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
||||||
|
Long tenantId, Long membershipId, String appCode, String accountStatus) {
|
||||||
|
this(accountId, username, null, password, displayName, tenantId, membershipId, appCode, accountStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public record ChangeLoginIdentifiersRequest(
|
||||||
|
@NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
|
||||||
|
@NotBlank @Pattern(regexp = "^1\\d{10}$") String phone,
|
||||||
|
@NotBlank String currentPassword) { }
|
||||||
|
public record UpdateAccountPhoneRequest(
|
||||||
|
@NotBlank @Pattern(regexp = "^1\\d{10}$") String phone) { }
|
||||||
public record UpdateAccountProfileRequest(@NotBlank @Size(max = 64) String displayName) { }
|
public record UpdateAccountProfileRequest(@NotBlank @Size(max = 64) String displayName) { }
|
||||||
public record UpdateAccountStatusRequest(@NotBlank String status) { }
|
public record UpdateAccountStatusRequest(@NotBlank String status) { }
|
||||||
public record RevokeAccountSessionsRequest(
|
public record RevokeAccountSessionsRequest(
|
||||||
@@ -56,10 +67,10 @@ public final class AccountManagementDtos {
|
|||||||
public record ImpersonationExchangeRequest(@NotBlank String code) { }
|
public record ImpersonationExchangeRequest(@NotBlank String code) { }
|
||||||
public record UpdateAccountScopeRequest(@NotNull Long tenantId, @NotNull Long membershipId,
|
public record UpdateAccountScopeRequest(@NotNull Long tenantId, @NotNull Long membershipId,
|
||||||
@NotBlank @Pattern(regexp = "MERCHANT|PLATFORM") String appCode) { }
|
@NotBlank @Pattern(regexp = "MERCHANT|PLATFORM") String appCode) { }
|
||||||
public record AccountSummary(Long accountId, String username, String displayName, String status,
|
public record AccountSummary(Long accountId, String username, String phone, String displayName, String status,
|
||||||
LocalDateTime lastLoginAt, Long tenantId, Long membershipId, String appCode) {
|
LocalDateTime lastLoginAt, Long tenantId, Long membershipId, String appCode) {
|
||||||
public AccountSummary(Long accountId, String username, String displayName, String status) {
|
public AccountSummary(Long accountId, String username, String displayName, String status) {
|
||||||
this(accountId, username, displayName, status, null, null, null, null);
|
this(accountId, username, null, displayName, status, null, null, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ package com.baiyue.auth.dto;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录请求契约,接收账号、密码和目标应用域等统一登录参数。
|
* 登录请求契约,接收账号、密码和目标应用域等统一登录参数。
|
||||||
*/
|
*/
|
||||||
@Schema(name = "LoginRequest", description = "Unified login credentials / 统一登录凭据")
|
@Schema(name = "LoginRequest", description = "Unified login credentials / 统一登录凭据")
|
||||||
public record LoginRequest(
|
public record LoginRequest(
|
||||||
@Schema(description = "Login username / 登录用户名", example = "merchant_admin") @NotBlank String username,
|
@Schema(description = "Username or mainland China mobile number / 用户名或中国大陆手机号", example = "merchant_admin")
|
||||||
|
@NotBlank @Size(max = 64) String loginId,
|
||||||
@Schema(description = "Login password / 登录密码", example = "ChangeMe123!", format = "password") @NotBlank String password
|
@Schema(description = "Login password / 登录密码", example = "ChangeMe123!", format = "password") @NotBlank String password
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import jakarta.validation.constraints.Size;
|
|||||||
|
|
||||||
@Schema(name = "MerchantRegisterRequest", description = "Phase-one merchant account registration")
|
@Schema(name = "MerchantRegisterRequest", description = "Phase-one merchant account registration")
|
||||||
public record MerchantRegisterRequest(
|
public record MerchantRegisterRequest(
|
||||||
@NotBlank @Pattern(regexp = "[A-Za-z0-9_]{4,64}") String username,
|
@NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
|
||||||
@NotBlank @Size(min = 8, max = 72) String password,
|
@NotBlank @Size(min = 8, max = 72) String password,
|
||||||
@NotBlank @Size(max = 64) String displayName,
|
@NotBlank @Size(max = 64) String displayName,
|
||||||
@NotBlank @Size(max = 128) String companyName,
|
@NotBlank @Size(max = 128) String companyName,
|
||||||
@NotBlank @Size(max = 64) String legalPerson,
|
@NotBlank @Size(max = 64) String legalPerson,
|
||||||
@NotBlank @Size(max = 32) String legalPhone,
|
@NotBlank @Pattern(regexp = "^1\\d{10}$") String legalPhone,
|
||||||
@Email @Size(max = 128) String contactEmail,
|
@Email @Size(max = 128) String contactEmail,
|
||||||
@Size(min = 2, max = 2) String countryCode,
|
@Size(min = 2, max = 2) String countryCode,
|
||||||
@Size(max = 255) String address
|
@Size(max = 255) String address
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import java.time.LocalDateTime;
|
|||||||
public class AuthAccount {
|
public class AuthAccount {
|
||||||
@TableId private Long id;
|
@TableId private Long id;
|
||||||
private String username;
|
private String username;
|
||||||
|
private String phone;
|
||||||
private String passwordHash;
|
private String passwordHash;
|
||||||
private String displayName;
|
private String displayName;
|
||||||
private String accountStatus;
|
private String accountStatus;
|
||||||
|
|||||||
@@ -22,11 +22,34 @@ public class AuthAccountRepository {
|
|||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthAccount findByPhone(String phone) {
|
||||||
|
return mapper.selectOne(new LambdaQueryWrapper<AuthAccount>()
|
||||||
|
.eq(AuthAccount::getPhone, phone)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
public boolean existsByUsername(String username) {
|
public boolean existsByUsername(String username) {
|
||||||
return mapper.selectCount(new LambdaQueryWrapper<AuthAccount>()
|
return mapper.selectCount(new LambdaQueryWrapper<AuthAccount>()
|
||||||
.eq(AuthAccount::getUsername, username)) > 0;
|
.eq(AuthAccount::getUsername, username)) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean existsByPhone(String phone) {
|
||||||
|
return mapper.selectCount(new LambdaQueryWrapper<AuthAccount>()
|
||||||
|
.eq(AuthAccount::getPhone, phone)) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean existsByUsernameForOtherAccount(String username, Long accountId) {
|
||||||
|
return mapper.selectCount(new LambdaQueryWrapper<AuthAccount>()
|
||||||
|
.eq(AuthAccount::getUsername, username)
|
||||||
|
.ne(AuthAccount::getId, accountId)) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean existsByPhoneForOtherAccount(String phone, Long accountId) {
|
||||||
|
return mapper.selectCount(new LambdaQueryWrapper<AuthAccount>()
|
||||||
|
.eq(AuthAccount::getPhone, phone)
|
||||||
|
.ne(AuthAccount::getId, accountId)) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public void insert(AuthAccount account) { mapper.insert(account); }
|
public void insert(AuthAccount account) { mapper.insert(account); }
|
||||||
|
|
||||||
public AuthAccount findById(Long id) {
|
public AuthAccount findById(Long id) {
|
||||||
|
|||||||
@@ -71,9 +71,13 @@ public class AccountManagementService {
|
|||||||
if (repository.existsByUsername(request.username())) {
|
if (repository.existsByUsername(request.username())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "登录账号已存在");
|
throw new BizException(ResultCode.CONFLICT, "登录账号已存在");
|
||||||
}
|
}
|
||||||
|
if (request.phone() != null && repository.existsByPhone(request.phone())) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "手机号已存在");
|
||||||
|
}
|
||||||
AuthAccount account = new AuthAccount();
|
AuthAccount account = new AuthAccount();
|
||||||
account.setId(request.accountId());
|
account.setId(request.accountId());
|
||||||
account.setUsername(request.username());
|
account.setUsername(request.username());
|
||||||
|
account.setPhone(request.phone());
|
||||||
account.setPasswordHash(passwordEncoder.encode(request.password()));
|
account.setPasswordHash(passwordEncoder.encode(request.password()));
|
||||||
account.setDisplayName(request.displayName());
|
account.setDisplayName(request.displayName());
|
||||||
account.setAccountStatus(request.accountStatus() == null ? "ACTIVE" : request.accountStatus());
|
account.setAccountStatus(request.accountStatus() == null ? "ACTIVE" : request.accountStatus());
|
||||||
@@ -117,6 +121,45 @@ public class AccountManagementService {
|
|||||||
bumpVersion(account);
|
bumpVersion(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@BusinessAudit(event = "ACCOUNT_LOGIN_IDENTIFIERS_CHANGE", resourceType = "ACCOUNT", resourceId = "#accountId",
|
||||||
|
description = "修改本人用户名和手机号", captureAfter = false)
|
||||||
|
public AccountSummary changeLoginIdentifiers(Long accountId, ChangeLoginIdentifiersRequest request) {
|
||||||
|
AuthAccount account = required(accountId);
|
||||||
|
if (!passwordEncoder.matches(request.currentPassword(), account.getPasswordHash())) {
|
||||||
|
throw new BizException(ResultCode.UNAUTHORIZED, "当前密码错误");
|
||||||
|
}
|
||||||
|
String username = request.username().trim();
|
||||||
|
String phone = request.phone().trim();
|
||||||
|
if (repository.existsByUsernameForOtherAccount(username, accountId)) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "用户名已存在");
|
||||||
|
}
|
||||||
|
if (repository.existsByPhoneForOtherAccount(phone, accountId)) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "手机号已存在");
|
||||||
|
}
|
||||||
|
if (Objects.equals(account.getUsername(), username) && Objects.equals(account.getPhone(), phone)) {
|
||||||
|
return summary(account);
|
||||||
|
}
|
||||||
|
account.setUsername(username);
|
||||||
|
account.setPhone(phone);
|
||||||
|
bumpVersion(account);
|
||||||
|
return summary(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@BusinessAudit(event = "ACCOUNT_PHONE_SYNC", resourceType = "ACCOUNT", resourceId = "#accountId",
|
||||||
|
description = "同步受管账号手机号", captureAfter = false)
|
||||||
|
public void updatePhone(Long accountId, UpdateAccountPhoneRequest request) {
|
||||||
|
AuthAccount account = required(accountId);
|
||||||
|
String phone = request.phone().trim();
|
||||||
|
if (Objects.equals(account.getPhone(), phone)) return;
|
||||||
|
if (repository.existsByPhoneForOtherAccount(phone, accountId)) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "手机号已存在");
|
||||||
|
}
|
||||||
|
account.setPhone(phone);
|
||||||
|
bumpVersion(account);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@BusinessAudit(event = "PASSWORD_RESET", resourceType = "ACCOUNT", resourceId = "#accountId", description = "管理员重置密码", captureAfter = false)
|
@BusinessAudit(event = "PASSWORD_RESET", resourceType = "ACCOUNT", resourceId = "#accountId", description = "管理员重置密码", captureAfter = false)
|
||||||
public void resetPassword(Long accountId, ResetPasswordRequest request, boolean forceChangePassword) {
|
public void resetPassword(Long accountId, ResetPasswordRequest request, boolean forceChangePassword) {
|
||||||
@@ -185,7 +228,7 @@ public class AccountManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AccountSummary summary(AuthAccount a) {
|
private AccountSummary summary(AuthAccount a) {
|
||||||
return new AccountSummary(a.getId(), a.getUsername(), a.getDisplayName(), a.getAccountStatus(),
|
return new AccountSummary(a.getId(), a.getUsername(), a.getPhone(), a.getDisplayName(), a.getAccountStatus(),
|
||||||
a.getLastLoginAt(), a.getDefaultTenantId(), a.getDefaultMembershipId(), a.getDefaultAppCode());
|
a.getLastLoginAt(), a.getDefaultTenantId(), a.getDefaultMembershipId(), a.getDefaultAppCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +244,7 @@ public class AccountManagementService {
|
|||||||
|
|
||||||
private boolean sameIdentity(AuthAccount account, CreateAccountRequest request, String appCode) {
|
private boolean sameIdentity(AuthAccount account, CreateAccountRequest request, String appCode) {
|
||||||
return Objects.equals(account.getUsername(), request.username())
|
return Objects.equals(account.getUsername(), request.username())
|
||||||
|
&& Objects.equals(account.getPhone(), request.phone())
|
||||||
&& Objects.equals(account.getDefaultTenantId(), request.tenantId())
|
&& Objects.equals(account.getDefaultTenantId(), request.tenantId())
|
||||||
&& Objects.equals(account.getOperatorTenantId(), request.tenantId())
|
&& Objects.equals(account.getOperatorTenantId(), request.tenantId())
|
||||||
&& Objects.equals(account.getDefaultMembershipId(), request.membershipId())
|
&& Objects.equals(account.getDefaultMembershipId(), request.membershipId())
|
||||||
|
|||||||
@@ -57,10 +57,13 @@ public class AuthService {
|
|||||||
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@BusinessAudit(event = "AUTH_LOGIN", resourceType = "ACCOUNT", resourceId = "#request.username",
|
@BusinessAudit(event = "AUTH_LOGIN", resourceType = "ACCOUNT", resourceId = "#request.loginId",
|
||||||
description = "User login / 用户登录", captureAfter = false)
|
description = "User login / 用户登录", captureAfter = false)
|
||||||
public TokenResponse login(LoginRequest request) {
|
public TokenResponse login(LoginRequest request) {
|
||||||
AuthAccount account = accountRepository.findByUsername(request.username());
|
String loginId = request.loginId().trim();
|
||||||
|
AuthAccount account = loginId.matches("^1\\d{10}$")
|
||||||
|
? accountRepository.findByPhone(loginId)
|
||||||
|
: accountRepository.findByUsername(loginId);
|
||||||
if (account == null) throw invalidCredentials();
|
if (account == null) throw invalidCredentials();
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
if (!"ACTIVE".equals(account.getAccountStatus()))
|
if (!"ACTIVE".equals(account.getAccountStatus()))
|
||||||
@@ -211,7 +214,7 @@ public class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static BizException invalidCredentials() {
|
private static BizException invalidCredentials() {
|
||||||
return new BizException(ResultCode.UNAUTHORIZED, "用户名或密码错误");
|
return new BizException(ResultCode.UNAUTHORIZED, "登录标识或密码错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ public class MerchantRegistrationService {
|
|||||||
resourceId = "#request.username", description = "商户注册", captureAfter = false)
|
resourceId = "#request.username", description = "商户注册", captureAfter = false)
|
||||||
public MerchantRegisterResponse register(MerchantRegisterRequest request) {
|
public MerchantRegisterResponse register(MerchantRegisterRequest request) {
|
||||||
if (accountRepository.existsByUsername(request.username())) {
|
if (accountRepository.existsByUsername(request.username())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "Username already exists");
|
throw new BizException(ResultCode.CONFLICT, "用户名已存在");
|
||||||
|
}
|
||||||
|
if (accountRepository.existsByPhone(request.legalPhone())) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "手机号已存在");
|
||||||
}
|
}
|
||||||
long accountId = IdWorker.getId();
|
long accountId = IdWorker.getId();
|
||||||
String passwordHash = passwordEncoder.encode(request.password());
|
String passwordHash = passwordEncoder.encode(request.password());
|
||||||
@@ -81,6 +84,7 @@ public class MerchantRegistrationService {
|
|||||||
AuthAccount account = new AuthAccount();
|
AuthAccount account = new AuthAccount();
|
||||||
account.setId(accountId);
|
account.setId(accountId);
|
||||||
account.setUsername(request.username());
|
account.setUsername(request.username());
|
||||||
|
account.setPhone(request.legalPhone());
|
||||||
account.setPasswordHash(passwordHash);
|
account.setPasswordHash(passwordHash);
|
||||||
account.setDisplayName(request.displayName());
|
account.setDisplayName(request.displayName());
|
||||||
account.setAccountStatus("ACTIVE");
|
account.setAccountStatus("ACTIVE");
|
||||||
@@ -94,7 +98,7 @@ public class MerchantRegistrationService {
|
|||||||
try {
|
try {
|
||||||
accountRepository.insert(account);
|
accountRepository.insert(account);
|
||||||
} catch (DuplicateKeyException exception) {
|
} catch (DuplicateKeyException exception) {
|
||||||
BizException failure = new BizException(ResultCode.CONFLICT, "Username already exists");
|
BizException failure = new BizException(ResultCode.CONFLICT, "用户名或手机号已存在");
|
||||||
failure.addSuppressed(exception);
|
failure.addSuppressed(exception);
|
||||||
compensateOnboarding(accountId, failure, compensationCompleted);
|
compensateOnboarding(accountId, failure, compensationCompleted);
|
||||||
throw failure;
|
throw failure;
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
-- Auth 单一登录标识增量:用户名或中国大陆手机号。
|
||||||
|
-- 可重复执行;手机号仅从 IAM 当前用户档案回填,无法确认的账号保持 NULL。
|
||||||
|
USE baiyue_auth;
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS auth_add_login_phone;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE auth_add_login_phone()
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'baiyue_auth' AND table_name = 'auth_account' AND column_name = 'phone') THEN
|
||||||
|
ALTER TABLE auth_account ADD COLUMN phone VARCHAR(32) NULL COMMENT '全局唯一中国大陆登录手机号' AFTER username;
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
CALL auth_add_login_phone();
|
||||||
|
DROP PROCEDURE auth_add_login_phone;
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS auth_backfill_login_phone;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE PROCEDURE auth_backfill_login_phone()
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT user.phone
|
||||||
|
FROM baiyue_iam.iam_user user
|
||||||
|
WHERE user.deleted = 0 AND user.phone REGEXP '^1[0-9]{10}$'
|
||||||
|
GROUP BY user.phone HAVING COUNT(DISTINCT user.auth_user_id) > 1) THEN
|
||||||
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'IAM 存在重复手机号,禁止建立 Auth 唯一登录索引';
|
||||||
|
END IF;
|
||||||
|
UPDATE auth_account account
|
||||||
|
JOIN baiyue_iam.iam_user user ON user.auth_user_id = account.id AND user.deleted = 0
|
||||||
|
SET account.phone = user.phone
|
||||||
|
WHERE account.phone IS NULL AND user.phone REGEXP '^1[0-9]{10}$';
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.statistics
|
||||||
|
WHERE table_schema = 'baiyue_auth' AND table_name = 'auth_account'
|
||||||
|
AND index_name = 'uk_auth_account_phone') THEN
|
||||||
|
ALTER TABLE auth_account ADD UNIQUE KEY uk_auth_account_phone (phone);
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
CALL auth_backfill_login_phone();
|
||||||
|
DROP PROCEDURE auth_backfill_login_phone;
|
||||||
@@ -6,6 +6,7 @@ USE baiyue_auth;
|
|||||||
CREATE TABLE IF NOT EXISTS auth_account (
|
CREATE TABLE IF NOT EXISTS auth_account (
|
||||||
id BIGINT NOT NULL COMMENT '统一登录账号ID(雪花ID)',
|
id BIGINT NOT NULL COMMENT '统一登录账号ID(雪花ID)',
|
||||||
username VARCHAR(64) NOT NULL COMMENT '全局唯一登录名',
|
username VARCHAR(64) NOT NULL COMMENT '全局唯一登录名',
|
||||||
|
phone VARCHAR(32) DEFAULT NULL COMMENT '全局唯一中国大陆登录手机号',
|
||||||
password_hash VARCHAR(255) NOT NULL COMMENT 'BCrypt密码哈希,禁止保存明文密码',
|
password_hash VARCHAR(255) NOT NULL COMMENT 'BCrypt密码哈希,禁止保存明文密码',
|
||||||
display_name VARCHAR(64) NOT NULL COMMENT '登录后展示名称',
|
display_name VARCHAR(64) NOT NULL COMMENT '登录后展示名称',
|
||||||
account_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '账号状态:ACTIVE正常、DISABLED停用、LOCKED锁定',
|
account_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '账号状态:ACTIVE正常、DISABLED停用、LOCKED锁定',
|
||||||
@@ -23,6 +24,7 @@ CREATE TABLE IF NOT EXISTS auth_account (
|
|||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uk_auth_account_username (username),
|
UNIQUE KEY uk_auth_account_username (username),
|
||||||
|
UNIQUE KEY uk_auth_account_phone (phone),
|
||||||
KEY idx_auth_account_status (account_status, locked_until)
|
KEY idx_auth_account_status (account_status, locked_until)
|
||||||
) ENGINE=InnoDB COMMENT='统一登录账号表:保存平台端和商户端共用的认证凭据及账号安全状态';
|
) ENGINE=InnoDB COMMENT='统一登录账号表:保存平台端和商户端共用的认证凭据及账号安全状态';
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.baiyue.auth.dto.AccountManagementDtos.CreateAccountRequest;
|
|||||||
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountProfileRequest;
|
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountProfileRequest;
|
||||||
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountStatusRequest;
|
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountStatusRequest;
|
||||||
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountScopeRequest;
|
import com.baiyue.auth.dto.AccountManagementDtos.UpdateAccountScopeRequest;
|
||||||
|
import com.baiyue.auth.dto.AccountManagementDtos.ChangeLoginIdentifiersRequest;
|
||||||
import com.baiyue.auth.entity.AuthAccount;
|
import com.baiyue.auth.entity.AuthAccount;
|
||||||
import com.baiyue.auth.repository.AuthAccountRepository;
|
import com.baiyue.auth.repository.AuthAccountRepository;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
@@ -157,6 +158,33 @@ class AccountManagementServiceTest {
|
|||||||
verify(sessions).revokeAll(10L);
|
verify(sessions).revokeAll(10L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test void changeLoginIdentifiersRequiresCurrentPasswordAndRevokesSessions() {
|
||||||
|
account.setUsername("old.user");
|
||||||
|
account.setPhone("13800138000");
|
||||||
|
when(repository.existsByUsernameForOtherAccount("new.user", 10L)).thenReturn(false);
|
||||||
|
when(repository.existsByPhoneForOtherAccount("13900139000", 10L)).thenReturn(false);
|
||||||
|
|
||||||
|
var result = service.changeLoginIdentifiers(10L,
|
||||||
|
new ChangeLoginIdentifiersRequest("new.user", "13900139000", "OldPass1"));
|
||||||
|
|
||||||
|
assertEquals("new.user", result.username());
|
||||||
|
assertEquals("13900139000", result.phone());
|
||||||
|
assertEquals(3, account.getTokenVersion());
|
||||||
|
verify(sessions).revokeAll(10L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void duplicatePhoneDoesNotChangeLoginIdentifiers() {
|
||||||
|
account.setUsername("old.user");
|
||||||
|
account.setPhone("13800138000");
|
||||||
|
when(repository.existsByUsernameForOtherAccount("new.user", 10L)).thenReturn(false);
|
||||||
|
when(repository.existsByPhoneForOtherAccount("13900139000", 10L)).thenReturn(true);
|
||||||
|
|
||||||
|
assertThrows(BizException.class, () -> service.changeLoginIdentifiers(10L,
|
||||||
|
new ChangeLoginIdentifiersRequest("new.user", "13900139000", "OldPass1")));
|
||||||
|
verify(repository, never()).save(any());
|
||||||
|
verify(sessions, never()).revokeAll(any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test void rbacChangeRevokesAccessAndRefreshSessionsAndBumpsTokenVersions() {
|
@Test void rbacChangeRevokesAccessAndRefreshSessionsAndBumpsTokenVersions() {
|
||||||
AuthAccount second = new AuthAccount();
|
AuthAccount second = new AuthAccount();
|
||||||
second.setId(11L); second.setTokenVersion(7); second.setAccountStatus("ACTIVE");
|
second.setId(11L); second.setTokenVersion(7); second.setAccountStatus("ACTIVE");
|
||||||
|
|||||||
@@ -100,6 +100,34 @@ class AuthServiceSmallJwtTest {
|
|||||||
assertThat(response.overseasExpiresIn()).isEqualTo(900);
|
assertThat(response.overseasExpiresIn()).isEqualTo(900);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mainlandPhoneIsResolvedAsTheSinglePhoneLoginIdentifier() {
|
||||||
|
AuthAccount account = new AuthAccount();
|
||||||
|
account.setId(42L);
|
||||||
|
account.setUsername("operator");
|
||||||
|
account.setPhone("13800138000");
|
||||||
|
account.setPasswordHash("hash");
|
||||||
|
account.setAccountStatus("ACTIVE");
|
||||||
|
account.setDefaultTenantId(0L);
|
||||||
|
account.setOperatorTenantId(0L);
|
||||||
|
account.setDefaultMembershipId(84L);
|
||||||
|
account.setDefaultAppCode("PLATFORM");
|
||||||
|
account.setTokenVersion(1);
|
||||||
|
AuthorizationSnapshot snapshot = new AuthorizationSnapshot(42L, 0L, 84L, "PLATFORM",
|
||||||
|
"WEB", Set.of(), Set.of(), 1L);
|
||||||
|
Jwt encoded = org.mockito.Mockito.mock(Jwt.class);
|
||||||
|
when(repository.findByPhone("13800138000")).thenReturn(account);
|
||||||
|
when(passwordEncoder.matches("secret", "hash")).thenReturn(true);
|
||||||
|
when(authorizationLoader.load(account)).thenReturn(snapshot);
|
||||||
|
when(jwtEncoder.encode(any())).thenReturn(encoded);
|
||||||
|
when(encoded.getTokenValue()).thenReturn("small-token");
|
||||||
|
|
||||||
|
service.login(new LoginRequest("13800138000", "secret"));
|
||||||
|
|
||||||
|
verify(repository).findByPhone("13800138000");
|
||||||
|
verify(repository, never()).findByUsername("13800138000");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void refreshRotatesDomesticAndOverseasTokensFromOneAuthorizationSnapshot() {
|
void refreshRotatesDomesticAndOverseasTokensFromOneAuthorizationSnapshot() {
|
||||||
AuthAccount account = new AuthAccount();
|
AuthAccount account = new AuthAccount();
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ public interface AuthAccountClient {
|
|||||||
@PathVariable Long id, @RequestBody StatusRequest request);
|
@PathVariable Long id, @RequestBody StatusRequest request);
|
||||||
@PutMapping("/{id}/profile") ApiResponse<Void> profile(@RequestHeader("X-Internal-Service-Token") String token,
|
@PutMapping("/{id}/profile") ApiResponse<Void> profile(@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
@PathVariable Long id, @RequestBody ProfileRequest request);
|
@PathVariable Long id, @RequestBody ProfileRequest request);
|
||||||
|
@PutMapping("/{id}/login-identifiers") ApiResponse<AccountSummary> changeLoginIdentifiers(
|
||||||
|
@RequestHeader("X-Internal-Service-Token") String token, @PathVariable Long id,
|
||||||
|
@RequestBody ChangeLoginIdentifiersRequest request);
|
||||||
|
@PutMapping("/{id}/phone") ApiResponse<Void> phone(@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
|
@PathVariable Long id, @RequestBody PhoneRequest request);
|
||||||
@PutMapping("/{id}/password") ApiResponse<Void> reset(@RequestHeader("X-Internal-Service-Token") String token,
|
@PutMapping("/{id}/password") ApiResponse<Void> reset(@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
@PathVariable Long id, @RequestBody PasswordRequest request);
|
@PathVariable Long id, @RequestBody PasswordRequest request);
|
||||||
/** Idempotently disables and revokes all accounts belonging to a customer. */
|
/** Idempotently disables and revokes all accounts belonging to a customer. */
|
||||||
@@ -37,25 +42,35 @@ public interface AuthAccountClient {
|
|||||||
@DeleteMapping("/{id}/compensation") ApiResponse<Void> compensate(@RequestHeader("X-Internal-Service-Token") String token,
|
@DeleteMapping("/{id}/compensation") ApiResponse<Void> compensate(@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
@PathVariable Long id);
|
@PathVariable Long id);
|
||||||
|
|
||||||
record CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
record CreateAccountRequest(Long accountId, String username, String phone, String password, String displayName,
|
||||||
Long tenantId, Long membershipId, String appCode, String accountStatus) {
|
Long tenantId, Long membershipId, String appCode, String accountStatus) {
|
||||||
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
||||||
Long tenantId, Long membershipId) {
|
Long tenantId, Long membershipId) {
|
||||||
this(accountId, username, password, displayName, tenantId, membershipId, "MERCHANT", "ACTIVE");
|
this(accountId, username, null, password, displayName, tenantId, membershipId, "MERCHANT", "ACTIVE");
|
||||||
}
|
}
|
||||||
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
||||||
Long tenantId, Long membershipId, String appCode) {
|
Long tenantId, Long membershipId, String appCode) {
|
||||||
this(accountId, username, password, displayName, tenantId, membershipId, appCode, "ACTIVE");
|
this(accountId, username, null, password, displayName, tenantId, membershipId, appCode, "ACTIVE");
|
||||||
|
}
|
||||||
|
public CreateAccountRequest(Long accountId, String username, String password, String displayName,
|
||||||
|
Long tenantId, Long membershipId, String appCode, String accountStatus) {
|
||||||
|
this(accountId, username, null, password, displayName, tenantId, membershipId, appCode, accountStatus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
record AccountSummary(Long accountId, String username, String displayName, String status,
|
record AccountSummary(Long accountId, String username, String phone, String displayName, String status,
|
||||||
LocalDateTime lastLoginAt, Long tenantId, Long membershipId, String appCode) {
|
LocalDateTime lastLoginAt, Long tenantId, Long membershipId, String appCode) {
|
||||||
public AccountSummary(Long accountId, String username, String displayName, String status) {
|
public AccountSummary(Long accountId, String username, String displayName, String status) {
|
||||||
this(accountId, username, displayName, status, null, null, null, null);
|
this(accountId, username, null, displayName, status, null, null, null, null);
|
||||||
|
}
|
||||||
|
public AccountSummary(Long accountId, String username, String displayName, String status,
|
||||||
|
LocalDateTime lastLoginAt, Long tenantId, Long membershipId, String appCode) {
|
||||||
|
this(accountId, username, null, displayName, status, lastLoginAt, tenantId, membershipId, appCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
record StatusRequest(String status) { }
|
record StatusRequest(String status) { }
|
||||||
record ProfileRequest(String displayName) { }
|
record ProfileRequest(String displayName) { }
|
||||||
|
record ChangeLoginIdentifiersRequest(String username, String phone, String currentPassword) { }
|
||||||
|
record PhoneRequest(String phone) { }
|
||||||
record PasswordRequest(String newPassword, boolean forceChangePassword) {
|
record PasswordRequest(String newPassword, boolean forceChangePassword) {
|
||||||
public PasswordRequest(String newPassword) { this(newPassword, false); }
|
public PasswordRequest(String newPassword) { this(newPassword, false); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
|||||||
import com.baiyue.iam.dto.MemberDtos.*;
|
import com.baiyue.iam.dto.MemberDtos.*;
|
||||||
import com.baiyue.iam.dto.ProfileDtos.PersonalProfileResponse;
|
import com.baiyue.iam.dto.ProfileDtos.PersonalProfileResponse;
|
||||||
import com.baiyue.iam.dto.ProfileDtos.UpdatePersonalProfileRequest;
|
import com.baiyue.iam.dto.ProfileDtos.UpdatePersonalProfileRequest;
|
||||||
|
import com.baiyue.iam.dto.ProfileDtos.ChangeLoginIdentifiersRequest;
|
||||||
import com.baiyue.iam.dto.TenantDtos.TenantResponse;
|
import com.baiyue.iam.dto.TenantDtos.TenantResponse;
|
||||||
import com.baiyue.iam.dto.TenantDtos.UpdateProfileRequest;
|
import com.baiyue.iam.dto.TenantDtos.UpdateProfileRequest;
|
||||||
import com.baiyue.iam.service.MemberService;
|
import com.baiyue.iam.service.MemberService;
|
||||||
@@ -54,6 +55,18 @@ public class MerchantTenantController {
|
|||||||
return ApiResponse.success(personalProfileService.update(tenantId, user.userId(), user.username(), request));
|
return ApiResponse.success(personalProfileService.update(tenantId, user.userId(), user.username(), request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PutMapping("/profile/login-identifiers")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
@Operation(summary = "修改当前登录用户的用户名和手机号",
|
||||||
|
description = "必须使用当前密码重新认证;成功后吊销全部会话,客户端需重新登录。")
|
||||||
|
public ApiResponse<PersonalProfileResponse> changeLoginIdentifiers(
|
||||||
|
@Valid @RequestBody ChangeLoginIdentifiersRequest request,
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||||
|
TenantGuard.rejectImpersonated(user);
|
||||||
|
long tenantId = TenantGuard.tenant(user, "MERCHANT");
|
||||||
|
return ApiResponse.success(personalProfileService.changeLoginIdentifiers(tenantId, user.userId(), request));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/tenant/profile")
|
@GetMapping("/tenant/profile")
|
||||||
@PreAuthorize("hasAuthority('merchant:profile:view')")
|
@PreAuthorize("hasAuthority('merchant:profile:view')")
|
||||||
@Operation(summary = "Get current enterprise profile / 查询当前企业资料", description = "Tenant ID is taken only from the authenticated merchant JWT. / 租户 ID 仅从已认证商户 JWT 获取。")
|
@Operation(summary = "Get current enterprise profile / 查询当前企业资料", description = "Tenant ID is taken only from the authenticated merchant JWT. / 租户 ID 仅从已认证商户 JWT 获取。")
|
||||||
@@ -74,7 +87,7 @@ public class MerchantTenantController {
|
|||||||
|
|
||||||
@PostMapping("/tenant/qualifications/submit-review")
|
@PostMapping("/tenant/qualifications/submit-review")
|
||||||
@PreAuthorize("hasAuthority('merchant:qualification:upload')")
|
@PreAuthorize("hasAuthority('merchant:qualification:upload')")
|
||||||
@Operation(summary = "提交当前企业资质审核", description = "校验营业执照和法人身份证齐全后创建审核单;货代资质证书和银行开户许可证选填。上传或替换文件不会自动送审。")
|
@Operation(summary = "提交当前企业资质审核", description = "校验营业执照齐全后创建审核单;法人身份证、货代资质证书和银行开户许可证选填。上传或替换文件不会自动送审。")
|
||||||
public ApiResponse<TenantResponse> submitQualificationReview(@AuthenticationPrincipal AuthenticatedUser user) {
|
public ApiResponse<TenantResponse> submitQualificationReview(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||||
return ApiResponse.success(tenantService.submitMerchantReview(TenantGuard.tenant(user, "MERCHANT")));
|
return ApiResponse.success(tenantService.submitMerchantReview(TenantGuard.tenant(user, "MERCHANT")));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ public final class MemberDtos {
|
|||||||
private MemberDtos() { }
|
private MemberDtos() { }
|
||||||
|
|
||||||
public record CreateMemberRequest(
|
public record CreateMemberRequest(
|
||||||
@NotBlank @Pattern(regexp = "^[A-Za-z0-9_.@-]{4,64}$") String username,
|
@NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
|
||||||
@NotBlank @Size(min = 8, max = 64)
|
@NotBlank @Size(min = 8, max = 64)
|
||||||
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
|
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字") String password,
|
||||||
@NotBlank @Size(max = 64) String displayName,
|
@NotBlank @Size(max = 64) String displayName,
|
||||||
@Size(max = 32) String phone,
|
@Pattern(regexp = "^1\\d{10}$") String phone,
|
||||||
@Email @Size(max = 128) String email,
|
@Email @Size(max = 128) String email,
|
||||||
@NotBlank String memberType,
|
@NotBlank String memberType,
|
||||||
@Size(max = 64) String memberNo,
|
@Size(max = 64) String memberNo,
|
||||||
@@ -30,7 +30,7 @@ public final class MemberDtos {
|
|||||||
|
|
||||||
public record UpdateMemberRequest(
|
public record UpdateMemberRequest(
|
||||||
@NotBlank @Size(max = 64) String displayName,
|
@NotBlank @Size(max = 64) String displayName,
|
||||||
@Size(max = 32) String phone,
|
@Pattern(regexp = "^1\\d{10}$") String phone,
|
||||||
@Email @Size(max = 128) String email,
|
@Email @Size(max = 128) String email,
|
||||||
@NotBlank String memberType,
|
@NotBlank String memberType,
|
||||||
@Size(max = 64) String memberNo,
|
@Size(max = 64) String memberNo,
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ public final class OnboardingDtos {
|
|||||||
@NotBlank String businessLicenseDocumentUrl,
|
@NotBlank String businessLicenseDocumentUrl,
|
||||||
LocalDate businessLicenseValidTo,
|
LocalDate businessLicenseValidTo,
|
||||||
boolean replaceBusinessLicense,
|
boolean replaceBusinessLicense,
|
||||||
@NotBlank String legalPersonIdDocumentUrl,
|
String legalPersonIdDocumentUrl,
|
||||||
LocalDate legalPersonIdValidTo,
|
LocalDate legalPersonIdValidTo,
|
||||||
boolean replaceLegalPersonId
|
Boolean replaceLegalPersonId
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public record RequiredQualificationPackageResponse(
|
public record RequiredQualificationPackageResponse(
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ public final class PlatformInternalUserDtos {
|
|||||||
Long authUserId,
|
Long authUserId,
|
||||||
String userType,
|
String userType,
|
||||||
@NotBlank @Size(max = 64) String displayName,
|
@NotBlank @Size(max = 64) String displayName,
|
||||||
@Size(max = 32) String phone,
|
@Pattern(regexp = "^1\\d{10}$") String phone,
|
||||||
@Email @Size(max = 128) String email,
|
@Email @Size(max = 128) String email,
|
||||||
@Pattern(regexp = "^[A-Za-z0-9_.@-]{4,64}$") String username,
|
@Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
|
||||||
@Size(min = 8, max = 64)
|
@Size(min = 8, max = 64)
|
||||||
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字")
|
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$", message = "密码必须同时包含字母和数字")
|
||||||
String password,
|
String password,
|
||||||
@@ -31,7 +31,7 @@ public final class PlatformInternalUserDtos {
|
|||||||
|
|
||||||
public record UpdateInternalUserRequest(
|
public record UpdateInternalUserRequest(
|
||||||
@NotBlank @Size(max = 64) String displayName,
|
@NotBlank @Size(max = 64) String displayName,
|
||||||
@Size(max = 32) String phone,
|
@Pattern(regexp = "^1\\d{10}$") String phone,
|
||||||
@Email @Size(max = 128) String email,
|
@Email @Size(max = 128) String email,
|
||||||
@NotBlank @Size(max = 64) String memberNo,
|
@NotBlank @Size(max = 64) String memberNo,
|
||||||
@NotNull List<Long> roleIds) { }
|
@NotNull List<Long> roleIds) { }
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public final class PlatformUserManagementDtos {
|
|||||||
private PlatformUserManagementDtos() { }
|
private PlatformUserManagementDtos() { }
|
||||||
|
|
||||||
public record CreateUserRequest(
|
public record CreateUserRequest(
|
||||||
@NotBlank @Pattern(regexp = "^[A-Za-z0-9_.@-]{4,64}$") String username,
|
@NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
|
||||||
@NotBlank @Size(max = 64) String displayName,
|
@NotBlank @Size(max = 64) String displayName,
|
||||||
@NotBlank @Pattern(regexp = "^1\\d{10}$", message = "手机号格式不正确") String phone,
|
@NotBlank @Pattern(regexp = "^1\\d{10}$", message = "手机号格式不正确") String phone,
|
||||||
@Email @Size(max = 128) String email,
|
@Email @Size(max = 128) String email,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.baiyue.iam.dto;
|
|||||||
|
|
||||||
import jakarta.validation.constraints.Email;
|
import jakarta.validation.constraints.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
/** 用户档案、地址和资质的数据契约。 */
|
/** 用户档案、地址和资质的数据契约。 */
|
||||||
@@ -12,9 +13,13 @@ public final class ProfileDtos {
|
|||||||
String displayName, String phone, String email) { }
|
String displayName, String phone, String email) { }
|
||||||
|
|
||||||
public record UpdatePersonalProfileRequest(@NotBlank @Size(max = 64) String displayName,
|
public record UpdatePersonalProfileRequest(@NotBlank @Size(max = 64) String displayName,
|
||||||
@Size(max = 32) String phone,
|
|
||||||
@Email @Size(max = 128) String email) { }
|
@Email @Size(max = 128) String email) { }
|
||||||
|
|
||||||
|
public record ChangeLoginIdentifiersRequest(
|
||||||
|
@NotBlank @Pattern(regexp = "^[A-Za-z][A-Za-z0-9_.@-]{3,63}$") String username,
|
||||||
|
@NotBlank @Pattern(regexp = "^1\\d{10}$") String phone,
|
||||||
|
@NotBlank String currentPassword) { }
|
||||||
|
|
||||||
public record AddressRequest(@NotBlank String addressType, String countryCode, String province,
|
public record AddressRequest(@NotBlank String addressType, String countryCode, String province,
|
||||||
String city, String district, @NotBlank String detail,
|
String city, String district, @NotBlank String detail,
|
||||||
String postalCode, Boolean isDefault) { }
|
String postalCode, Boolean isDefault) { }
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ public class MemberService {
|
|||||||
long userId = IdWorker.getId();
|
long userId = IdWorker.getId();
|
||||||
long membershipId = IdWorker.getId();
|
long membershipId = IdWorker.getId();
|
||||||
call(() -> authClient.create(internalToken, new AuthAccountClient.CreateAccountRequest(
|
call(() -> authClient.create(internalToken, new AuthAccountClient.CreateAccountRequest(
|
||||||
authId, request.username(), request.password(), request.displayName(), tenantId, membershipId)));
|
authId, request.username(), request.phone(), request.password(), request.displayName(),
|
||||||
|
tenantId, membershipId, "MERCHANT", "ACTIVE")));
|
||||||
try {
|
try {
|
||||||
IamUser user = new IamUser();
|
IamUser user = new IamUser();
|
||||||
user.setId(userId); user.setAuthUserId(authId); user.setUserType("ENTERPRISE");
|
user.setId(userId); user.setAuthUserId(authId); user.setUserType("ENTERPRISE");
|
||||||
@@ -134,6 +135,10 @@ public class MemberService {
|
|||||||
throw new BizException(ResultCode.FORBIDDEN, "企业所有者不允许通过成员管理接口编辑");
|
throw new BizException(ResultCode.FORBIDDEN, "企业所有者不允许通过成员管理接口编辑");
|
||||||
}
|
}
|
||||||
IamUser user = requiredUser(membership.getUserId());
|
IamUser user = requiredUser(membership.getUserId());
|
||||||
|
if (!java.util.Objects.equals(user.getPhone(), request.phone())) {
|
||||||
|
call(() -> authClient.phone(internalToken, user.getAuthUserId(),
|
||||||
|
new AuthAccountClient.PhoneRequest(request.phone())));
|
||||||
|
}
|
||||||
user.setDisplayName(request.displayName()); user.setPhone(request.phone()); user.setEmail(request.email());
|
user.setDisplayName(request.displayName()); user.setPhone(request.phone()); user.setEmail(request.email());
|
||||||
membership.setMemberType(request.memberType()); membership.setMemberNo(request.memberNo());
|
membership.setMemberType(request.memberType()); membership.setMemberNo(request.memberNo());
|
||||||
repository.saveUser(user); repository.save(membership);
|
repository.saveUser(user); repository.save(membership);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import com.baiyue.common.core.api.ResultCode;
|
|||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.iam.dto.ProfileDtos.PersonalProfileResponse;
|
import com.baiyue.iam.dto.ProfileDtos.PersonalProfileResponse;
|
||||||
import com.baiyue.iam.dto.ProfileDtos.UpdatePersonalProfileRequest;
|
import com.baiyue.iam.dto.ProfileDtos.UpdatePersonalProfileRequest;
|
||||||
|
import com.baiyue.iam.dto.ProfileDtos.ChangeLoginIdentifiersRequest;
|
||||||
|
import com.baiyue.iam.client.AuthAccountClient;
|
||||||
import com.baiyue.iam.entity.IamMembership;
|
import com.baiyue.iam.entity.IamMembership;
|
||||||
import com.baiyue.iam.entity.IamUser;
|
import com.baiyue.iam.entity.IamUser;
|
||||||
import com.baiyue.iam.repository.MemberRepository;
|
import com.baiyue.iam.repository.MemberRepository;
|
||||||
@@ -11,12 +13,19 @@ import jakarta.annotation.Resource;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import com.baiyue.common.audit.BusinessAudit;
|
import com.baiyue.common.audit.BusinessAudit;
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import feign.FeignException;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|
||||||
/** 当前用户个人资料服务,必须同时匹配认证账号与当前租户成员关系。 */
|
/** 当前用户个人资料服务,必须同时匹配认证账号与当前租户成员关系。 */
|
||||||
@Service
|
@Service
|
||||||
public class PersonalProfileService {
|
public class PersonalProfileService {
|
||||||
@Resource
|
@Resource
|
||||||
private MemberRepository repository;
|
private MemberRepository repository;
|
||||||
|
@Resource
|
||||||
|
private AuthAccountClient authClient;
|
||||||
|
@Value("${baiyue.security.internal-service-token}")
|
||||||
|
private String internalToken;
|
||||||
|
|
||||||
public PersonalProfileResponse get(long tenantId, Long authUserId, String username) {
|
public PersonalProfileResponse get(long tenantId, Long authUserId, String username) {
|
||||||
return response(tenantId, authUserId, username);
|
return response(tenantId, authUserId, username);
|
||||||
@@ -30,12 +39,40 @@ public class PersonalProfileService {
|
|||||||
if (membership == null || !"ACTIVE".equals(membership.getStatus())) throw forbidden();
|
if (membership == null || !"ACTIVE".equals(membership.getStatus())) throw forbidden();
|
||||||
IamUser user = repository.findUser(membership.getUserId());
|
IamUser user = repository.findUser(membership.getUserId());
|
||||||
if (user == null || !authUserId.equals(user.getAuthUserId())) throw forbidden();
|
if (user == null || !authUserId.equals(user.getAuthUserId())) throw forbidden();
|
||||||
user.setDisplayName(request.displayName()); user.setPhone(request.phone()); user.setEmail(request.email());
|
user.setDisplayName(request.displayName()); user.setEmail(request.email());
|
||||||
repository.saveUser(user);
|
repository.saveUser(user);
|
||||||
return new PersonalProfileResponse(user.getId(), authUserId, username,
|
return new PersonalProfileResponse(user.getId(), authUserId, username,
|
||||||
user.getDisplayName(), user.getPhone(), user.getEmail());
|
user.getDisplayName(), user.getPhone(), user.getEmail());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@BusinessAudit(event = "PERSONAL_LOGIN_IDENTIFIERS_UPDATE", resourceType = "IAM_USER",
|
||||||
|
resourceId = "#authUserId", description = "修改本人用户名和手机号", captureAfter = false)
|
||||||
|
public PersonalProfileResponse changeLoginIdentifiers(long tenantId, Long authUserId,
|
||||||
|
ChangeLoginIdentifiersRequest request) {
|
||||||
|
IamMembership membership = repository.findByTenantAndAuthUser(tenantId, authUserId);
|
||||||
|
if (membership == null || !"ACTIVE".equals(membership.getStatus())) throw forbidden();
|
||||||
|
IamUser user = repository.findUser(membership.getUserId());
|
||||||
|
if (user == null || !authUserId.equals(user.getAuthUserId())) throw forbidden();
|
||||||
|
AuthAccountClient.AccountSummary account;
|
||||||
|
try {
|
||||||
|
ApiResponse<AuthAccountClient.AccountSummary> result = authClient.changeLoginIdentifiers(
|
||||||
|
internalToken, authUserId, new AuthAccountClient.ChangeLoginIdentifiersRequest(
|
||||||
|
request.username(), request.phone(), request.currentPassword()));
|
||||||
|
if (result == null || !result.successful() || result.data() == null) {
|
||||||
|
ResultCode code = result == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(result.code());
|
||||||
|
throw new BizException(code, result == null ? "认证账号服务暂时不可用" : result.message());
|
||||||
|
}
|
||||||
|
account = result.data();
|
||||||
|
} catch (FeignException ex) {
|
||||||
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用");
|
||||||
|
}
|
||||||
|
user.setPhone(account.phone());
|
||||||
|
repository.saveUser(user);
|
||||||
|
return new PersonalProfileResponse(user.getId(), authUserId, account.username(),
|
||||||
|
user.getDisplayName(), user.getPhone(), user.getEmail());
|
||||||
|
}
|
||||||
|
|
||||||
private PersonalProfileResponse response(long tenantId, Long authUserId, String username) {
|
private PersonalProfileResponse response(long tenantId, Long authUserId, String username) {
|
||||||
IamMembership membership = repository.findByTenantAndAuthUser(tenantId, authUserId);
|
IamMembership membership = repository.findByTenantAndAuthUser(tenantId, authUserId);
|
||||||
if (membership == null) throw forbidden();
|
if (membership == null) throw forbidden();
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ public class PlatformInternalUserService {
|
|||||||
validateMemberNo(memberNo, null);
|
validateMemberNo(memberNo, null);
|
||||||
|
|
||||||
call(() -> authClient.create(internalToken, new AuthAccountClient.CreateAccountRequest(
|
call(() -> authClient.create(internalToken, new AuthAccountClient.CreateAccountRequest(
|
||||||
authUserId, request.username(), request.password(), request.displayName(),
|
authUserId, request.username(), request.phone(), request.password(), request.displayName(),
|
||||||
PLATFORM_TENANT_ID, membershipId, "PLATFORM")));
|
PLATFORM_TENANT_ID, membershipId, "PLATFORM", "ACTIVE")));
|
||||||
try {
|
try {
|
||||||
IamUser user = newUser(userId, authUserId, request.displayName(), request.phone(), request.email());
|
IamUser user = newUser(userId, authUserId, request.displayName(), request.phone(), request.email());
|
||||||
IamMembership membership = newMembership(membershipId, userId, memberNo);
|
IamMembership membership = newMembership(membershipId, userId, memberNo);
|
||||||
@@ -96,12 +96,17 @@ public class PlatformInternalUserService {
|
|||||||
IamUser user = requiredUser(membership.getUserId());
|
IamUser user = requiredUser(membership.getUserId());
|
||||||
AuthAccountClient.AccountSummary oldAccount = account(user.getAuthUserId());
|
AuthAccountClient.AccountSummary oldAccount = account(user.getAuthUserId());
|
||||||
boolean displayNameChanged = !Objects.equals(user.getDisplayName(), request.displayName());
|
boolean displayNameChanged = !Objects.equals(user.getDisplayName(), request.displayName());
|
||||||
|
boolean phoneChanged = !Objects.equals(user.getPhone(), request.phone());
|
||||||
List<Long> oldRoleIds = repository.findRoleIds(PLATFORM_TENANT_ID, membershipId);
|
List<Long> oldRoleIds = repository.findRoleIds(PLATFORM_TENANT_ID, membershipId);
|
||||||
boolean rolesChanged = !new java.util.HashSet<>(oldRoleIds).equals(new java.util.HashSet<>(roleIds));
|
boolean rolesChanged = !new java.util.HashSet<>(oldRoleIds).equals(new java.util.HashSet<>(roleIds));
|
||||||
if (displayNameChanged) {
|
if (displayNameChanged) {
|
||||||
call(() -> authClient.profile(internalToken, user.getAuthUserId(),
|
call(() -> authClient.profile(internalToken, user.getAuthUserId(),
|
||||||
new AuthAccountClient.ProfileRequest(request.displayName())));
|
new AuthAccountClient.ProfileRequest(request.displayName())));
|
||||||
}
|
}
|
||||||
|
if (phoneChanged) {
|
||||||
|
call(() -> authClient.phone(internalToken, user.getAuthUserId(),
|
||||||
|
new AuthAccountClient.PhoneRequest(request.phone())));
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
user.setDisplayName(request.displayName());
|
user.setDisplayName(request.displayName());
|
||||||
user.setPhone(request.phone());
|
user.setPhone(request.phone());
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ public class PlatformUserManagementService {
|
|||||||
long membershipId = IdWorker.getId();
|
long membershipId = IdWorker.getId();
|
||||||
String appCode = appCode(tenantId);
|
String appCode = appCode(tenantId);
|
||||||
call(() -> authClient.create(internalToken, new AuthAccountClient.CreateAccountRequest(
|
call(() -> authClient.create(internalToken, new AuthAccountClient.CreateAccountRequest(
|
||||||
authUserId, request.username().trim(), request.password(), request.displayName().trim(),
|
authUserId, request.username().trim(), request.phone().trim(), request.password(),
|
||||||
tenantId, membershipId, appCode, request.status())));
|
request.displayName().trim(), tenantId, membershipId, appCode, request.status())));
|
||||||
try {
|
try {
|
||||||
IamUser user = new IamUser();
|
IamUser user = new IamUser();
|
||||||
user.setId(userId);
|
user.setId(userId);
|
||||||
@@ -137,12 +137,15 @@ public class PlatformUserManagementService {
|
|||||||
}
|
}
|
||||||
AuthAccountClient.AccountSummary oldAccount = account(user.getAuthUserId());
|
AuthAccountClient.AccountSummary oldAccount = account(user.getAuthUserId());
|
||||||
boolean profileChanged = !Objects.equals(user.getDisplayName(), request.displayName().trim());
|
boolean profileChanged = !Objects.equals(user.getDisplayName(), request.displayName().trim());
|
||||||
|
boolean phoneChanged = !Objects.equals(user.getPhone(), request.phone().trim());
|
||||||
boolean scopeChanged = oldTenantId != newTenantId;
|
boolean scopeChanged = oldTenantId != newTenantId;
|
||||||
boolean rolesChanged = !new java.util.HashSet<>(oldRoleIds).equals(new java.util.HashSet<>(roleIds));
|
boolean rolesChanged = !new java.util.HashSet<>(oldRoleIds).equals(new java.util.HashSet<>(roleIds));
|
||||||
boolean warehouseChanged = !Objects.equals(membership.getOverseasWarehouseId(), overseasWarehouseId);
|
boolean warehouseChanged = !Objects.equals(membership.getOverseasWarehouseId(), overseasWarehouseId);
|
||||||
|
|
||||||
if (profileChanged) call(() -> authClient.profile(internalToken, user.getAuthUserId(),
|
if (profileChanged) call(() -> authClient.profile(internalToken, user.getAuthUserId(),
|
||||||
new AuthAccountClient.ProfileRequest(request.displayName().trim())));
|
new AuthAccountClient.ProfileRequest(request.displayName().trim())));
|
||||||
|
if (phoneChanged) call(() -> authClient.phone(internalToken, user.getAuthUserId(),
|
||||||
|
new AuthAccountClient.PhoneRequest(request.phone().trim())));
|
||||||
if (scopeChanged) call(() -> authClient.scope(internalToken, user.getAuthUserId(),
|
if (scopeChanged) call(() -> authClient.scope(internalToken, user.getAuthUserId(),
|
||||||
new AuthAccountClient.ScopeRequest(newTenantId, membershipId, appCode(newTenantId))));
|
new AuthAccountClient.ScopeRequest(newTenantId, membershipId, appCode(newTenantId))));
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -160,24 +160,32 @@ public class ProfileResourceService {
|
|||||||
List<IamTenantQualification> businessLicenses = currentQualifications(locked, "BUSINESS_LICENSE");
|
List<IamTenantQualification> businessLicenses = currentQualifications(locked, "BUSINESS_LICENSE");
|
||||||
List<IamTenantQualification> legalPersonIds = currentQualifications(locked, "LEGAL_PERSON_ID");
|
List<IamTenantQualification> legalPersonIds = currentQualifications(locked, "LEGAL_PERSON_ID");
|
||||||
requireReplaceAllowed(businessLicenses, request.replaceBusinessLicense(), "营业执照");
|
requireReplaceAllowed(businessLicenses, request.replaceBusinessLicense(), "营业执照");
|
||||||
requireReplaceAllowed(legalPersonIds, request.replaceLegalPersonId(), "法人身份证");
|
boolean hasLegalPersonId = StringUtils.hasText(request.legalPersonIdDocumentUrl());
|
||||||
|
if (hasLegalPersonId) {
|
||||||
|
requireReplaceAllowed(legalPersonIds, Boolean.TRUE.equals(request.replaceLegalPersonId()), "法人身份证");
|
||||||
|
} else if (Boolean.TRUE.equals(request.replaceLegalPersonId())) {
|
||||||
|
throw new BizException(ResultCode.BAD_REQUEST, "未上传法人身份证时不能选择替换");
|
||||||
|
}
|
||||||
|
|
||||||
String replacedBusinessLicenseUrl = supersede(businessLicenses, replacementComment);
|
String replacedBusinessLicenseUrl = supersede(businessLicenses, replacementComment);
|
||||||
String replacedLegalPersonIdUrl = supersede(legalPersonIds, replacementComment);
|
String replacedLegalPersonIdUrl = hasLegalPersonId ? supersede(legalPersonIds, replacementComment) : null;
|
||||||
IamTenantQualification businessLicense = createQualification(tenant.getId(), "BUSINESS_LICENSE",
|
IamTenantQualification businessLicense = createQualification(tenant.getId(), "BUSINESS_LICENSE",
|
||||||
creditCode, request.businessLicenseDocumentUrl());
|
creditCode, request.businessLicenseDocumentUrl());
|
||||||
businessLicense.setValidTo(request.businessLicenseValidTo());
|
businessLicense.setValidTo(request.businessLicenseValidTo());
|
||||||
repository.saveQualification(businessLicense);
|
repository.saveQualification(businessLicense);
|
||||||
IamTenantQualification legalPersonId = createQualification(tenant.getId(), "LEGAL_PERSON_ID",
|
IamTenantQualification legalPersonId = null;
|
||||||
null, request.legalPersonIdDocumentUrl());
|
if (hasLegalPersonId) {
|
||||||
legalPersonId.setValidTo(request.legalPersonIdValidTo());
|
legalPersonId = createQualification(tenant.getId(), "LEGAL_PERSON_ID",
|
||||||
repository.saveQualification(legalPersonId);
|
null, request.legalPersonIdDocumentUrl().trim());
|
||||||
|
legalPersonId.setValidTo(request.legalPersonIdValidTo());
|
||||||
|
repository.saveQualification(legalPersonId);
|
||||||
|
}
|
||||||
if (!creditCode.equals(tenant.getUnifiedSocialCreditCode())) {
|
if (!creditCode.equals(tenant.getUnifiedSocialCreditCode())) {
|
||||||
tenant.setUnifiedSocialCreditCode(creditCode);
|
tenant.setUnifiedSocialCreditCode(creditCode);
|
||||||
tenantRepository.saveTenant(tenant);
|
tenantRepository.saveTenant(tenant);
|
||||||
}
|
}
|
||||||
return new RequiredQualificationPackageResponse(
|
return new RequiredQualificationPackageResponse(
|
||||||
businessLicense.getId(), legalPersonId.getId(), tenant.getStatus(),
|
businessLicense.getId(), legalPersonId == null ? null : legalPersonId.getId(), tenant.getStatus(),
|
||||||
replacedBusinessLicenseUrl, replacedLegalPersonIdUrl);
|
replacedBusinessLicenseUrl, replacedLegalPersonIdUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,9 +214,9 @@ public class ProfileResourceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void requireOptionalQualificationType(String type) {
|
private void requireOptionalQualificationType(String type) {
|
||||||
if ("BUSINESS_LICENSE".equals(type) || "LEGAL_PERSON_ID".equals(type)) {
|
if ("BUSINESS_LICENSE".equals(type)) {
|
||||||
throw new BizException(ResultCode.BAD_REQUEST,
|
throw new BizException(ResultCode.BAD_REQUEST,
|
||||||
"Required qualifications must be uploaded as one package / 营业执照、统一社会信用代码和法人身份证必须整包上传");
|
"The business license must be uploaded in the required package / 营业执照和统一社会信用代码必须通过必填资质包上传");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -215,7 +215,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
|||||||
IamTenant tenant, long membershipId) {
|
IamTenant tenant, long membershipId) {
|
||||||
try {
|
try {
|
||||||
ApiResponse<AuthAccountClient.AccountSummary> response = authClient.create(internalToken,
|
ApiResponse<AuthAccountClient.AccountSummary> response = authClient.create(internalToken,
|
||||||
new AuthAccountClient.CreateAccountRequest(authUserId, username, password,
|
new AuthAccountClient.CreateAccountRequest(authUserId, username, tenant.getContactPhone(), password,
|
||||||
tenant.getContactName(), tenant.getId(), membershipId, "MERCHANT", "ACTIVE"));
|
tenant.getContactName(), tenant.getId(), membershipId, "MERCHANT", "ACTIVE"));
|
||||||
if (response == null) throw unavailable();
|
if (response == null) throw unavailable();
|
||||||
if (!response.successful()) {
|
if (!response.successful()) {
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ import java.util.stream.Collectors;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class TenantService {
|
public class TenantService {
|
||||||
private static final Set<String> REQUIRED_QUALIFICATION_TYPES = Set.of(
|
private static final Set<String> REQUIRED_QUALIFICATION_TYPES = Set.of("BUSINESS_LICENSE");
|
||||||
"BUSINESS_LICENSE", "LEGAL_PERSON_ID");
|
|
||||||
@Resource
|
@Resource
|
||||||
private TenantRepository repository;
|
private TenantRepository repository;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -456,7 +455,7 @@ public class TenantService {
|
|||||||
var qualifications = profileRepository.findQualifications(tenantId);
|
var qualifications = profileRepository.findQualifications(tenantId);
|
||||||
if (!hasAllPendingQualifications(qualifications)) {
|
if (!hasAllPendingQualifications(qualifications)) {
|
||||||
throw new BizException(ResultCode.BAD_REQUEST,
|
throw new BizException(ResultCode.BAD_REQUEST,
|
||||||
"Business license and legal-person ID are required / 营业执照和法人身份证为必填资质");
|
"Business license is required / 营业执照为必填资质");
|
||||||
}
|
}
|
||||||
IamTenantReview review = new IamTenantReview();
|
IamTenantReview review = new IamTenantReview();
|
||||||
review.setId(IdWorker.getId()); review.setTenantId(tenantId);
|
review.setId(IdWorker.getId()); review.setTenantId(tenantId);
|
||||||
|
|||||||
@@ -17,6 +17,56 @@ tags:
|
|||||||
- name: RBAC
|
- name: RBAC
|
||||||
description: Platform and merchant role-permission management / 平台端与商户端角色权限管理
|
description: Platform and merchant role-permission management / 平台端与商户端角色权限管理
|
||||||
paths:
|
paths:
|
||||||
|
/api/merchant/v1/iam/profile:
|
||||||
|
get:
|
||||||
|
tags: [Merchant Tenant / 商户企业]
|
||||||
|
summary: 查看当前登录用户个人资料
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 查询成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {$ref: '#/components/schemas/PersonalProfileApiResponse'}
|
||||||
|
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||||
|
'403': {$ref: '#/components/responses/Forbidden'}
|
||||||
|
put:
|
||||||
|
tags: [Merchant Tenant / 商户企业]
|
||||||
|
summary: 修改当前登录用户普通资料
|
||||||
|
description: 仅修改显示名称和邮箱;用户名、手机号必须走敏感标识接口。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {$ref: '#/components/schemas/UpdatePersonalProfileRequest'}
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 修改成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {$ref: '#/components/schemas/PersonalProfileApiResponse'}
|
||||||
|
'400': {$ref: '#/components/responses/BadRequest'}
|
||||||
|
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||||
|
'403': {$ref: '#/components/responses/Forbidden'}
|
||||||
|
/api/merchant/v1/iam/profile/login-identifiers:
|
||||||
|
put:
|
||||||
|
tags: [Merchant Tenant / 商户企业]
|
||||||
|
summary: 修改当前登录用户的用户名和手机号
|
||||||
|
description: 仅允许当前非代登录用户修改本人标识,必须用当前密码重新认证;成功后全部 Token 和会话失效,需重新登录。权限为已认证客户本人,无额外权限码。
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {$ref: '#/components/schemas/ChangeLoginIdentifiersRequest'}
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 修改成功并已吊销会话
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {$ref: '#/components/schemas/PersonalProfileApiResponse'}
|
||||||
|
'400': {$ref: '#/components/responses/BadRequest'}
|
||||||
|
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||||
|
'403': {$ref: '#/components/responses/Forbidden'}
|
||||||
|
'409': {$ref: '#/components/responses/Conflict'}
|
||||||
/api/platform/v1/iam/tenants/contact-phone-availability:
|
/api/platform/v1/iam/tenants/contact-phone-availability:
|
||||||
get:
|
get:
|
||||||
tags: [Platform Tenant / 平台租户]
|
tags: [Platform Tenant / 平台租户]
|
||||||
@@ -924,6 +974,35 @@ components:
|
|||||||
ServiceUnavailable: {description: Authentication service unavailable / 认证服务不可用, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
ServiceUnavailable: {description: Authentication service unavailable / 认证服务不可用, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||||
UnprocessableEntity: {description: 参数校验失败, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
UnprocessableEntity: {description: 参数校验失败, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||||
schemas:
|
schemas:
|
||||||
|
PersonalProfileResponse:
|
||||||
|
type: object
|
||||||
|
required: [userId, authUserId, username, displayName, phone]
|
||||||
|
properties:
|
||||||
|
userId: {type: string, pattern: '^\d+$'}
|
||||||
|
authUserId: {type: string, pattern: '^\d+$'}
|
||||||
|
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
|
||||||
|
displayName: {type: string, maxLength: 64}
|
||||||
|
phone: {type: string, pattern: '^1\d{10}$'}
|
||||||
|
email: {type: string, format: email, nullable: true}
|
||||||
|
UpdatePersonalProfileRequest:
|
||||||
|
type: object
|
||||||
|
required: [displayName]
|
||||||
|
properties:
|
||||||
|
displayName: {type: string, minLength: 1, maxLength: 64}
|
||||||
|
email: {type: string, format: email, maxLength: 128, nullable: true}
|
||||||
|
ChangeLoginIdentifiersRequest:
|
||||||
|
type: object
|
||||||
|
required: [username, phone, currentPassword]
|
||||||
|
properties:
|
||||||
|
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
|
||||||
|
phone: {type: string, pattern: '^1\d{10}$'}
|
||||||
|
currentPassword: {type: string, format: password, writeOnly: true}
|
||||||
|
PersonalProfileApiResponse:
|
||||||
|
allOf:
|
||||||
|
- {$ref: '#/components/schemas/ApiResponse'}
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data: {$ref: '#/components/schemas/PersonalProfileResponse'}
|
||||||
ApiResponse:
|
ApiResponse:
|
||||||
type: object
|
type: object
|
||||||
required: [code, message, timestamp]
|
required: [code, message, timestamp]
|
||||||
@@ -1206,22 +1285,22 @@ components:
|
|||||||
reviewComment: {type: string}
|
reviewComment: {type: string}
|
||||||
RequiredQualificationPackageRequest:
|
RequiredQualificationPackageRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [tenantId, unifiedSocialCreditCode, businessLicenseDocumentUrl, replaceBusinessLicense, legalPersonIdDocumentUrl, replaceLegalPersonId]
|
required: [tenantId, unifiedSocialCreditCode, businessLicenseDocumentUrl, replaceBusinessLicense]
|
||||||
properties:
|
properties:
|
||||||
tenantId: {type: string, pattern: '^\d+$'}
|
tenantId: {type: string, pattern: '^\d+$'}
|
||||||
unifiedSocialCreditCode: {type: string, pattern: '^[0-9A-Za-z]{18}$'}
|
unifiedSocialCreditCode: {type: string, pattern: '^[0-9A-Za-z]{18}$'}
|
||||||
businessLicenseDocumentUrl: {type: string, minLength: 1}
|
businessLicenseDocumentUrl: {type: string, minLength: 1}
|
||||||
businessLicenseValidTo: {type: string, format: date, nullable: true}
|
businessLicenseValidTo: {type: string, format: date, nullable: true}
|
||||||
replaceBusinessLicense: {type: boolean}
|
replaceBusinessLicense: {type: boolean}
|
||||||
legalPersonIdDocumentUrl: {type: string, minLength: 1}
|
legalPersonIdDocumentUrl: {type: string, minLength: 1, nullable: true, description: 法人身份证选填}
|
||||||
legalPersonIdValidTo: {type: string, format: date, nullable: true}
|
legalPersonIdValidTo: {type: string, format: date, nullable: true}
|
||||||
replaceLegalPersonId: {type: boolean}
|
replaceLegalPersonId: {type: boolean, nullable: true, default: false}
|
||||||
RequiredQualificationPackageResponse:
|
RequiredQualificationPackageResponse:
|
||||||
type: object
|
type: object
|
||||||
required: [businessLicenseQualificationId, legalPersonIdQualificationId, tenantStatus]
|
required: [businessLicenseQualificationId, tenantStatus]
|
||||||
properties:
|
properties:
|
||||||
businessLicenseQualificationId: {type: string, pattern: '^\d+$'}
|
businessLicenseQualificationId: {type: string, pattern: '^\d+$'}
|
||||||
legalPersonIdQualificationId: {type: string, pattern: '^\d+$'}
|
legalPersonIdQualificationId: {type: string, pattern: '^\d+$', nullable: true}
|
||||||
tenantStatus: {type: string, enum: [PROFILE_REQUIRED, REJECTED]}
|
tenantStatus: {type: string, enum: [PROFILE_REQUIRED, REJECTED]}
|
||||||
replacedBusinessLicenseDocumentUrl: {type: string, nullable: true}
|
replacedBusinessLicenseDocumentUrl: {type: string, nullable: true}
|
||||||
replacedLegalPersonIdDocumentUrl: {type: string, nullable: true}
|
replacedLegalPersonIdDocumentUrl: {type: string, nullable: true}
|
||||||
@@ -1317,7 +1396,7 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
required: [username, displayName, phone, tenantId, roleIds, password, status]
|
required: [username, displayName, phone, tenantId, roleIds, password, status]
|
||||||
properties:
|
properties:
|
||||||
username: {type: string, minLength: 4, maxLength: 64}
|
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
|
||||||
displayName: {type: string, maxLength: 64}
|
displayName: {type: string, maxLength: 64}
|
||||||
phone: {type: string, pattern: '^1\\d{10}$'}
|
phone: {type: string, pattern: '^1\\d{10}$'}
|
||||||
email: {type: string, format: email, maxLength: 128}
|
email: {type: string, format: email, maxLength: 128}
|
||||||
@@ -1342,10 +1421,10 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
authUserId: {type: integer, format: int64, description: 兼容旧接口;填写后关联已有认证账号}
|
authUserId: {type: integer, format: int64, description: 兼容旧接口;填写后关联已有认证账号}
|
||||||
userType: {type: string, enum: [INTERNAL, PLATFORM], description: 兼容旧接口,可省略}
|
userType: {type: string, enum: [INTERNAL, PLATFORM], description: 兼容旧接口,可省略}
|
||||||
username: {type: string, minLength: 4, maxLength: 64}
|
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
|
||||||
password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true}
|
password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true}
|
||||||
displayName: {type: string, maxLength: 64}
|
displayName: {type: string, maxLength: 64}
|
||||||
phone: {type: string, maxLength: 32}
|
phone: {type: string, pattern: '^1\d{10}$'}
|
||||||
email: {type: string, format: email, maxLength: 128}
|
email: {type: string, format: email, maxLength: 128}
|
||||||
memberNo: {type: string, maxLength: 64}
|
memberNo: {type: string, maxLength: 64}
|
||||||
roleIds: {type: array, items: {type: integer, format: int64}}
|
roleIds: {type: array, items: {type: integer, format: int64}}
|
||||||
@@ -1354,7 +1433,7 @@ components:
|
|||||||
required: [displayName, memberNo, roleIds]
|
required: [displayName, memberNo, roleIds]
|
||||||
properties:
|
properties:
|
||||||
displayName: {type: string, maxLength: 64}
|
displayName: {type: string, maxLength: 64}
|
||||||
phone: {type: string, maxLength: 32}
|
phone: {type: string, pattern: '^1\d{10}$'}
|
||||||
email: {type: string, format: email, maxLength: 128}
|
email: {type: string, format: email, maxLength: 128}
|
||||||
memberNo: {type: string, maxLength: 64}
|
memberNo: {type: string, maxLength: 64}
|
||||||
roleIds: {type: array, items: {type: integer, format: int64}}
|
roleIds: {type: array, items: {type: integer, format: int64}}
|
||||||
@@ -1413,10 +1492,10 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
required: [username, password, displayName, memberType]
|
required: [username, password, displayName, memberType]
|
||||||
properties:
|
properties:
|
||||||
username: {type: string, minLength: 4, maxLength: 64}
|
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
|
||||||
password: {type: string, format: password, minLength: 8, maxLength: 64}
|
password: {type: string, format: password, minLength: 8, maxLength: 64}
|
||||||
displayName: {type: string, maxLength: 64}
|
displayName: {type: string, maxLength: 64}
|
||||||
phone: {type: string, maxLength: 32}
|
phone: {type: string, pattern: '^1\d{10}$'}
|
||||||
email: {type: string, format: email, maxLength: 128}
|
email: {type: string, format: email, maxLength: 128}
|
||||||
memberType: {type: string, enum: [EMPLOYEE]}
|
memberType: {type: string, enum: [EMPLOYEE]}
|
||||||
memberNo: {type: string}
|
memberNo: {type: string}
|
||||||
@@ -1429,7 +1508,7 @@ components:
|
|||||||
required: [displayName, memberType]
|
required: [displayName, memberType]
|
||||||
properties:
|
properties:
|
||||||
displayName: {type: string, maxLength: 64}
|
displayName: {type: string, maxLength: 64}
|
||||||
phone: {type: string, maxLength: 32}
|
phone: {type: string, pattern: '^1\d{10}$'}
|
||||||
email: {type: string, format: email, maxLength: 128}
|
email: {type: string, format: email, maxLength: 128}
|
||||||
memberType: {type: string, enum: [EMPLOYEE]}
|
memberType: {type: string, enum: [EMPLOYEE]}
|
||||||
memberNo: {type: string, maxLength: 64}
|
memberNo: {type: string, maxLength: 64}
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ class MemberServiceTest {
|
|||||||
@Test void shouldHandleCase08() {
|
@Test void shouldHandleCase08() {
|
||||||
IamRole role = activeMerchantRole(50L, 100L);
|
IamRole role = activeMerchantRole(50L, 100L);
|
||||||
when(roleMapper.selectById(50L)).thenReturn(role);
|
when(roleMapper.selectById(50L)).thenReturn(role);
|
||||||
|
when(auth.phone(eq("internal-token"), eq(40L), any()))
|
||||||
|
.thenReturn(ApiResponse.success(null));
|
||||||
UpdateMemberRequest request = new UpdateMemberRequest("新姓名", "13800138000", null,
|
UpdateMemberRequest request = new UpdateMemberRequest("新姓名", "13800138000", null,
|
||||||
"EMPLOYEE", "M-001", List.of(50L));
|
"EMPLOYEE", "M-001", List.of(50L));
|
||||||
|
|
||||||
@@ -169,6 +171,7 @@ class MemberServiceTest {
|
|||||||
assertEquals("新姓名", result.displayName());
|
assertEquals("新姓名", result.displayName());
|
||||||
verify(repository).saveUser(user);
|
verify(repository).saveUser(user);
|
||||||
verify(repository).replaceRoles(100L, 20L, List.of(50L));
|
verify(repository).replaceRoles(100L, 20L, List.of(50L));
|
||||||
|
verify(auth).phone(eq("internal-token"), eq(40L), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test void shouldHandleCase09() {
|
@Test void shouldHandleCase09() {
|
||||||
|
|||||||
@@ -83,6 +83,25 @@ class MerchantQualificationUploadTest {
|
|||||||
verify(tenantRepository).saveTenant(tenant);
|
verify(tenantRepository).saveTenant(tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requiredPackageAcceptsAnEmptyOptionalLegalPersonId() {
|
||||||
|
IamTenant tenant = new IamTenant();
|
||||||
|
tenant.setId(10001L);
|
||||||
|
tenant.setTenantType("MERCHANT");
|
||||||
|
tenant.setStatus("PROFILE_REQUIRED");
|
||||||
|
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||||
|
when(profileRepository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
|
||||||
|
|
||||||
|
var response = service.submitRequiredQualificationPackage(new RequiredQualificationPackageRequest(
|
||||||
|
10001L, "91310101abcde12345",
|
||||||
|
"/api/merchant/v1/files/qualifications/license.png", null, false,
|
||||||
|
null, null, null));
|
||||||
|
|
||||||
|
assertNull(response.legalPersonIdQualificationId());
|
||||||
|
verify(profileRepository, times(1)).insertQualification(any());
|
||||||
|
verify(tenantRepository).saveTenant(tenant);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void requiredPackageCannotOverwriteAnEstablishedCreditCode() {
|
void requiredPackageCannotOverwriteAnEstablishedCreditCode() {
|
||||||
IamTenant tenant = new IamTenant();
|
IamTenant tenant = new IamTenant();
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package com.baiyue.iam.service;
|
|||||||
|
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.iam.dto.ProfileDtos.UpdatePersonalProfileRequest;
|
import com.baiyue.iam.dto.ProfileDtos.UpdatePersonalProfileRequest;
|
||||||
|
import com.baiyue.iam.dto.ProfileDtos.ChangeLoginIdentifiersRequest;
|
||||||
|
import com.baiyue.iam.client.AuthAccountClient;
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
import com.baiyue.iam.entity.IamMembership;
|
import com.baiyue.iam.entity.IamMembership;
|
||||||
import com.baiyue.iam.entity.IamUser;
|
import com.baiyue.iam.entity.IamUser;
|
||||||
import com.baiyue.iam.repository.MemberRepository;
|
import com.baiyue.iam.repository.MemberRepository;
|
||||||
@@ -10,14 +13,17 @@ import org.junit.jupiter.api.Test;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
@DisplayName("个人资料服务")
|
@DisplayName("个人资料服务")
|
||||||
@SpringJUnitConfig(PersonalProfileService.class)
|
@SpringJUnitConfig(PersonalProfileService.class)
|
||||||
|
@TestPropertySource(properties = "baiyue.security.internal-service-token=test-internal-token")
|
||||||
class PersonalProfileServiceTest {
|
class PersonalProfileServiceTest {
|
||||||
@MockBean private MemberRepository repository;
|
@MockBean private MemberRepository repository;
|
||||||
|
@MockBean private AuthAccountClient authClient;
|
||||||
@Resource private PersonalProfileService service;
|
@Resource private PersonalProfileService service;
|
||||||
|
|
||||||
@Test void canOnlyModifyOwnProfileWithinCurrentTenant() {
|
@Test void canOnlyModifyOwnProfileWithinCurrentTenant() {
|
||||||
@@ -26,11 +32,29 @@ class PersonalProfileServiceTest {
|
|||||||
when(repository.findByTenantAndAuthUser(1L, 2L)).thenReturn(membership);
|
when(repository.findByTenantAndAuthUser(1L, 2L)).thenReturn(membership);
|
||||||
when(repository.findUser(3L)).thenReturn(user);
|
when(repository.findUser(3L)).thenReturn(user);
|
||||||
var result = service.update(1L, 2L, "tester",
|
var result = service.update(1L, 2L, "tester",
|
||||||
new UpdatePersonalProfileRequest("新姓名", "13800138000", "a@example.com"));
|
new UpdatePersonalProfileRequest("新姓名", "a@example.com"));
|
||||||
assertEquals("新姓名", result.displayName());
|
assertEquals("新姓名", result.displayName());
|
||||||
verify(repository).saveUser(user);
|
verify(repository).saveUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test void sensitiveLoginIdentifiersAreChangedOnlyThroughAuthReauthentication() {
|
||||||
|
IamMembership membership = new IamMembership(); membership.setUserId(3L); membership.setStatus("ACTIVE");
|
||||||
|
IamUser user = new IamUser(); user.setId(3L); user.setAuthUserId(2L); user.setDisplayName("测试用户");
|
||||||
|
when(repository.findByTenantAndAuthUser(1L, 2L)).thenReturn(membership);
|
||||||
|
when(repository.findUser(3L)).thenReturn(user);
|
||||||
|
when(authClient.changeLoginIdentifiers(any(), eq(2L), any())).thenReturn(ApiResponse.success(
|
||||||
|
new AuthAccountClient.AccountSummary(2L, "new.user", "13900139000", "测试用户", "ACTIVE",
|
||||||
|
null, 1L, 4L, "MERCHANT")));
|
||||||
|
|
||||||
|
var result = service.changeLoginIdentifiers(1L, 2L,
|
||||||
|
new ChangeLoginIdentifiersRequest("new.user", "13900139000", "CurrentPass1"));
|
||||||
|
|
||||||
|
assertEquals("new.user", result.username());
|
||||||
|
assertEquals("13900139000", result.phone());
|
||||||
|
verify(repository).saveUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
@Test void crossTenantIdentityIsRejected() {
|
@Test void crossTenantIdentityIsRejected() {
|
||||||
assertThrows(BizException.class, () -> service.get(9L, 2L, "tester"));
|
assertThrows(BizException.class, () -> service.get(9L, 2L, "tester"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,11 +369,11 @@ class TenantServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void explicitSubmissionRequiresOnlyBusinessLicenseAndLegalPersonIdAndCreatesReview() {
|
void explicitSubmissionRequiresBusinessLicenseAndCreatesReview() {
|
||||||
IamTenant tenant = merchant(10001L, "PROFILE_REQUIRED");
|
IamTenant tenant = merchant(10001L, "PROFILE_REQUIRED");
|
||||||
when(repository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
when(repository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||||
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
||||||
qualification("BUSINESS_LICENSE"), qualification("LEGAL_PERSON_ID")));
|
qualification("BUSINESS_LICENSE")));
|
||||||
|
|
||||||
var submitted = service.submitMerchantReview(10001L);
|
var submitted = service.submitMerchantReview(10001L);
|
||||||
|
|
||||||
@@ -383,10 +383,10 @@ class TenantServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void explicitSubmissionRejectsAnIncompleteQualificationSet() {
|
void explicitSubmissionRejectsMissingBusinessLicense() {
|
||||||
when(repository.findTenantForUpdate(10001L)).thenReturn(merchant(10001L, "PROFILE_REQUIRED"));
|
when(repository.findTenantForUpdate(10001L)).thenReturn(merchant(10001L, "PROFILE_REQUIRED"));
|
||||||
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
||||||
qualification("BUSINESS_LICENSE")));
|
qualification("LEGAL_PERSON_ID")));
|
||||||
|
|
||||||
BizException exception = assertThrows(BizException.class, () -> service.submitMerchantReview(10001L));
|
BizException exception = assertThrows(BizException.class, () -> service.submitMerchantReview(10001L));
|
||||||
|
|
||||||
@@ -480,8 +480,8 @@ class TenantServiceTest {
|
|||||||
void shouldHandleCase01() {
|
void shouldHandleCase01() {
|
||||||
when(repository.findTenant(10001L)).thenReturn(merchant(10001L, "ACTIVE"));
|
when(repository.findTenant(10001L)).thenReturn(merchant(10001L, "ACTIVE"));
|
||||||
when(profileRepository.findQualifications(10001L)).thenReturn(
|
when(profileRepository.findQualifications(10001L)).thenReturn(
|
||||||
List.of(approvedQualification("BUSINESS_LICENSE")),
|
List.of(),
|
||||||
List.of(approvedQualification("BUSINESS_LICENSE"), approvedQualification("LEGAL_PERSON_ID"),
|
List.of(approvedQualification("BUSINESS_LICENSE"),
|
||||||
rejectedQualification("FREIGHT_FORWARDER_CERTIFICATE")));
|
rejectedQualification("FREIGHT_FORWARDER_CERTIFICATE")));
|
||||||
|
|
||||||
var withoutQualification = service.orderEligibility(10001L);
|
var withoutQualification = service.orderEligibility(10001L);
|
||||||
|
|||||||
Reference in New Issue
Block a user