diff --git a/src/main/java/com/imyeyu/api/config/RedisConfig.java b/src/main/java/com/imyeyu/api/config/RedisConfig.java index 2ae799f..c381e7b 100644 --- a/src/main/java/com/imyeyu/api/config/RedisConfig.java +++ b/src/main/java/com/imyeyu/api/config/RedisConfig.java @@ -2,6 +2,7 @@ package com.imyeyu.api.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.imyeyu.api.modules.common.entity.Multilingual; +import com.imyeyu.api.modules.gao.bean.GaoUserInviteCache; import com.imyeyu.spring.bean.RedisConfigParams; import com.imyeyu.spring.config.AbstractRedisConfig; import com.imyeyu.spring.util.Redis; @@ -202,4 +203,10 @@ public class RedisConfig extends AbstractRedisConfig { public Redis getClipboardRedisTemplate() { return getRedis(database, RedisSerializers.STRING, RedisSerializers.STRING); } + + /// @return GAO 用户邀请,code: 邀请缓存 + @Bean("redisGaoUserInvite") + public Redis getGaoUserInviteRedisTemplate() { + return getRedis(database, RedisSerializers.STRING, RedisSerializers.jacksonSerializer(jackson, GaoUserInviteCache.class)); + } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/bean/GaoUserInviteCache.java b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoUserInviteCache.java new file mode 100644 index 0000000..0505b40 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/bean/GaoUserInviteCache.java @@ -0,0 +1,29 @@ +package com.imyeyu.api.modules.gao.bean; + +import lombok.Data; + +/// GAO 用户邀请缓存 +/// +/// @author Codex +/// @since 2026-08-13 +@Data +public class GaoUserInviteCache { + + /// 发起邀请的用户 ID + private String inviterUserId; + + /// 门店 ID + private String storeId; + + /// 角色 ID + private String roleId; + + /// 邀请来源或场景 + private String scene; + + /// 邀请创建时间,毫秒时间戳 + private Long createdAt; + + /// 邀请过期时间,毫秒时间戳 + private Long expireAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java index f045ea8..7e94011 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java +++ b/src/main/java/com/imyeyu/api/modules/gao/controller/GaoUserController.java @@ -9,9 +9,17 @@ import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; import com.imyeyu.api.modules.gao.bean.GaoRoleCode; import com.imyeyu.api.modules.gao.entity.GaoUser; import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.gao.service.GaoUserInviteService; import com.imyeyu.api.modules.gao.service.GaoUserService; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateResponse; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteRegisterData; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteRegisterRequest; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteValidateRequest; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteValidateResponse; import com.imyeyu.api.modules.user.entity.Role; import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.vo.LoginResponse; import com.imyeyu.api.modules.user.service.RoleChecker; import com.imyeyu.api.modules.user.service.RoleService; import com.imyeyu.api.modules.user.service.UserLoginService; @@ -19,10 +27,12 @@ import com.imyeyu.api.modules.user.service.UserRoleService; import com.imyeyu.api.modules.user.service.UserService; import com.imyeyu.java.TimiJava; import com.imyeyu.spring.annotation.AOPLog; +import com.imyeyu.spring.annotation.CaptchaValid; import com.imyeyu.spring.annotation.RequestRateLimit; import com.imyeyu.spring.bean.Page; import com.imyeyu.spring.bean.PageResult; import com.imyeyu.spring.util.ResponseView; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -32,6 +42,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Map; +import java.util.UUID; /// GAO 用户接口 /// @@ -46,11 +57,38 @@ public class GaoUserController { private final RoleService roleService; private final RoleChecker roleChecker; private final GaoUserService service; + private final GaoUserInviteService inviteService; private final UserRoleService userRoleService; private final GaoStoreService gaoStoreService; private final UserLoginService userLoginService; private final AttachmentService attachmentService; + @AOPLog + @RequestRateLimit + @RequireGaoPermission(GaoPermissionCode.USER_CREATE) + @PostMapping("/invite") + public GaoUserInviteCreateResponse invite(@RequestBody @Valid GaoUserInviteCreateRequest req) { + return inviteService.create(req); + } + + @AOPLog + @RequestRateLimit(value = 20, inSeconds = 10) + @PostMapping("/invite/validate") + public GaoUserInviteValidateResponse validateInvite(@RequestBody @Valid GaoUserInviteValidateRequest req) { + return inviteService.validate(req.getCode()); + } + + @AOPLog + @JsonView(ResponseView.Public.class) + @CaptchaValid + @RequestRateLimit(value = 5, inSeconds = 60) + @PostMapping("/invite/register") + public LoginResponse registerByInvite(@RequestBody @Valid GaoUserInviteRegisterRequest req) { + GaoUserInviteRegisterData data = req.getData(); + String userId = inviteService.register(data); + return userLoginService.login(UUID.randomUUID().toString(), userId); + } + @AOPLog @RequestRateLimit @RequireGaoPermission(GaoPermissionCode.USER_READ) @@ -137,6 +175,7 @@ public class GaoUserController { @RequireGaoPermission(GaoPermissionCode.USER_READ) @PostMapping("/role/list") public List roleList() { + // TODO 此接口获取本模块所有角色,需要限制角色 return roleService.listByModuleCode(ModuleCode.GAO); } } diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/GaoUserInviteService.java b/src/main/java/com/imyeyu/api/modules/gao/service/GaoUserInviteService.java new file mode 100644 index 0000000..51b466b --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/GaoUserInviteService.java @@ -0,0 +1,31 @@ +package com.imyeyu.api.modules.gao.service; + +import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateResponse; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteRegisterData; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteValidateResponse; + +/// GAO 用户邀请注册服务 +/// +/// @author Codex +/// @since 2026-08-13 +public interface GaoUserInviteService { + + /// 创建 GAO 用户邀请 + /// + /// @param req 邀请创建请求 + /// @return 邀请 code 和过期时间 + GaoUserInviteCreateResponse create(GaoUserInviteCreateRequest req); + + /// 校验 GAO 用户邀请是否可用 + /// + /// @param code 邀请 code + /// @return 校验结果 + GaoUserInviteValidateResponse validate(String code); + + /// 使用邀请注册 GAO 用户 + /// + /// @param data 注册数据 + /// @return 注册成功的用户 ID + String register(GaoUserInviteRegisterData data); +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserInviteServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserInviteServiceImplement.java new file mode 100644 index 0000000..69c39e2 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserInviteServiceImplement.java @@ -0,0 +1,325 @@ +package com.imyeyu.api.modules.gao.service.implement; + +import com.imyeyu.api.bean.ModuleCode; +import com.imyeyu.api.config.dbsource.TimiServerDBConfig; +import com.imyeyu.api.modules.gao.bean.GaoUserInviteCache; +import com.imyeyu.api.modules.gao.bean.GaoPermissionCode; +import com.imyeyu.api.modules.gao.bean.GaoRoleCode; +import com.imyeyu.api.modules.gao.entity.GaoStore; +import com.imyeyu.api.modules.gao.entity.GaoUser; +import com.imyeyu.api.modules.gao.service.GaoStoreService; +import com.imyeyu.api.modules.gao.service.GaoUserInviteService; +import com.imyeyu.api.modules.gao.service.GaoUserService; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateRequest; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteCreateResponse; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteRegisterData; +import com.imyeyu.api.modules.gao.vo.GaoUserInviteValidateResponse; +import com.imyeyu.api.modules.system.entity.Logger; +import com.imyeyu.api.modules.system.service.LoggerService; +import com.imyeyu.api.modules.user.entity.Role; +import com.imyeyu.api.modules.user.entity.User; +import com.imyeyu.api.modules.user.entity.UserRole; +import com.imyeyu.api.modules.user.service.AuthorizationScopeService; +import com.imyeyu.api.modules.user.service.PermissionChecker; +import com.imyeyu.api.modules.user.service.RoleChecker; +import com.imyeyu.api.modules.user.service.RoleService; +import com.imyeyu.api.modules.user.service.UserLoginService; +import com.imyeyu.api.modules.user.service.UserRoleService; +import com.imyeyu.api.modules.user.service.UserService; +import com.imyeyu.api.modules.user.bean.AuthOwnerType; +import com.imyeyu.java.TimiJava; +import com.imyeyu.java.bean.timi.TimiCode; +import com.imyeyu.java.bean.timi.TimiException; +import com.imyeyu.spring.util.Redis; +import com.imyeyu.utils.Digest; +import com.imyeyu.utils.Time; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.security.SecureRandom; +import java.util.List; +import java.util.UUID; + +/// GAO 用户邀请注册服务实现 +/// +/// @author Codex +/// @since 2026-08-13 +@Slf4j +@Service +@RequiredArgsConstructor +public class GaoUserInviteServiceImplement implements GaoUserInviteService { + + private static final String INVITE_KEY_PREFIX = "GAO:USER:INVITE:"; + private static final String CONSUME_LOCK_KEY_PREFIX = "GAO:USER:INVITE:CONSUME:"; + private static final String CREATE_CODE_LOCK_KEY = "GAO:USER:INVITE:CREATE:LOCK"; + private static final String CODE_CHARACTERS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + private static final int CODE_LENGTH = 10; + private static final long INVITE_TTL = Time.D * 7; + private static final long CODE_LOCK_TTL = Time.S * 5; + private static final long CONSUME_LOCK_TTL = Time.M * 2; + private static final long REGISTER_RATE_LIMIT_TTL = Time.M * 10; + private static final int CODE_RATE_LIMIT = 30; + private static final int REGISTER_VALUE_RATE_LIMIT = 10; + + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + private final GaoStoreService gaoStoreService; + private final GaoUserService gaoUserService; + private final UserLoginService userLoginService; + private final UserRoleService userRoleService; + private final UserService userService; + private final RoleService roleService; + private final PermissionChecker permissionChecker; + private final RoleChecker roleChecker; + private final AuthorizationScopeService authorizationScopeService; + private final LoggerService loggerService; + + @Qualifier("redisGaoUserInvite") + private final Redis redisInvite; + + @Qualifier("redisLocker") + private final Redis redisLocker; + + @Qualifier("redisRateLimit") + private final Redis redisRateLimit; + + @Override + public GaoUserInviteCreateResponse create(GaoUserInviteCreateRequest req) { + Logger logger = new Logger(Logger.Module.GAO, Logger.Level.INFO, "GAO_USER_INVITE_CREATE"); + try { + TimiException.required(req, "not found invite request"); + TimiException.required(req.getRoleId(), "not found roleId"); + + String inviterUserId = userLoginService.getRequireLoginUserId(); + logger.setUserId(inviterUserId); + permissionChecker.checkAny(ModuleCode.GAO, GaoPermissionCode.USER_CREATE.getValue()); + boolean globalManager = authorizationScopeService.isSystemAdmin(inviterUserId) + || roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name()); + boolean storeManager = !globalManager; + GaoStore loginStore = storeManager ? gaoStoreService.getByUserId(inviterUserId) : null; + String storeId = storeManager ? loginStore == null ? null : loginStore.getId() : req.getStoreId(); + TimiException.required(storeId, "not found storeId"); + checkStoreScope(inviterUserId, storeId); + + GaoStore store = requireActiveStore(storeId); + Role role = requireInvitableRole(inviterUserId, req.getRoleId(), storeId); + long createdAt = Time.now(); + long expireAt = createdAt + INVITE_TTL; + + GaoUserInviteCache cache = new GaoUserInviteCache(); + cache.setInviterUserId(inviterUserId); + cache.setStoreId(store.getId()); + cache.setRoleId(role.getId()); + cache.setScene(storeManager ? "STORE_MANAGER" : "STORE_MANAGEMENT"); + cache.setCreatedAt(createdAt); + cache.setExpireAt(expireAt); + String code = saveInvite(cache); + + logger.setContent("storeId=%s;roleId=%s;scene=%s;createdAt=%d;expireAt=%d".formatted( + store.getId(), role.getId(), cache.getScene(), createdAt, expireAt)); + logger.setResult("created"); + return new GaoUserInviteCreateResponse(code, expireAt); + } catch (TimiException e) { + logger.setLevel(Logger.Level.WARN); + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("GAO 邀请创建失败", e); + throw new TimiException(TimiCode.ERROR, "邀请创建失败", e); + } finally { + loggerService.create(logger); + } + } + + @Override + public GaoUserInviteValidateResponse validate(String code) { + TimiException.required(code, "not found code"); + checkCodeRateLimit(code); + GaoUserInviteCache cache = requireValidInvite(code); + requireActiveStore(cache.getStoreId()); + requireValidRole(cache.getRoleId(), cache.getStoreId()); + return new GaoUserInviteValidateResponse(true, cache.getExpireAt()); + } + + @Transactional(TimiServerDBConfig.ROLLBACKER) + @Override + public String register(GaoUserInviteRegisterData data) { + TimiException.required(data, "not found register data"); + String code = data.getCode(); + TimiException.required(code, "not found code"); + checkCodeRateLimit(code); + checkRegisterValueRateLimit("NAME", data.getName()); + checkRegisterValueRateLimit("PHONE", data.getPhoneNo()); + String lockKey = getConsumeLockKey(code); + TimiException.requiredTrue(redisLocker.lock(lockKey, 1, CONSUME_LOCK_TTL), "邀请码无效或正在使用"); + + GaoUserInviteCache cache = null; + Logger logger = new Logger(Logger.Module.GAO, Logger.Level.WARN, "GAO_USER_INVITE_REGISTER"); + try { + cache = requireValidInvite(code); + logger.setContent(buildLogContent(cache)); + requireActiveStore(cache.getStoreId()); + requireValidRole(cache.getRoleId(), cache.getStoreId()); + TimiException.requiredNull(userService.getByName(data.getName()), "existed user name"); + TimiException.requiredNull(userService.getByPhoneNo(data.getPhoneNo()), "existed phone number"); + if (!redisInvite.destroy(getInviteKey(code))) { + throw invalidInvite(); + } + + User user = new User(); + user.setName(data.getName()); + user.setPhoneNo(data.getPhoneNo()); + user.setPassword(data.getPassword()); + + GaoUser gaoUser = new GaoUser(); + gaoUser.setStoreId(cache.getStoreId()); + gaoUser.setUser(user); + gaoUserService.create(gaoUser); + + UserRole userRole = new UserRole(); + userRole.setId(UUID.randomUUID().toString()); + userRole.setUserId(user.getId()); + userRole.setRoleId(cache.getRoleId()); + userRole.setCreatedAt(Time.now()); + userRoleService.create(userRole); + + logger.setLevel(Logger.Level.INFO); + logger.setUserId(user.getId()); + logger.setResult(user.getId()); + return user.getId(); + } catch (TimiException e) { + logger.setException(e.getMessage()); + throw e; + } catch (Exception e) { + logger.setLevel(Logger.Level.ERROR); + logger.setException(TimiJava.serializeThrowable(e)); + log.error("GAO 用户邀请注册失败", e); + throw new TimiException(TimiCode.ERROR, "邀请注册失败", e); + } finally { + loggerService.create(logger); + redisLocker.releaseLock(lockKey); + } + } + + private String saveInvite(GaoUserInviteCache cache) { + TimiException.requiredTrue(redisLocker.lock(CREATE_CODE_LOCK_KEY, 1, CODE_LOCK_TTL), "服务繁忙,请稍后重试"); + try { + for (int attempt = 0; attempt < 3; attempt++) { + String code = randomCode(); + if (!redisInvite.has(getInviteKey(code))) { + redisInvite.set(getInviteKey(code), cache, INVITE_TTL); + return code; + } + } + throw new TimiException(TimiCode.ERROR_SERVICE_BUSY, "邀请码生成失败"); + } finally { + redisLocker.releaseLock(CREATE_CODE_LOCK_KEY); + } + } + + private String randomCode() { + StringBuilder code = new StringBuilder(CODE_LENGTH); + for (int i = 0; i < CODE_LENGTH; i++) { + code.append(CODE_CHARACTERS.charAt(SECURE_RANDOM.nextInt(CODE_CHARACTERS.length()))); + } + return code.toString(); + } + + private GaoUserInviteCache requireValidInvite(String code) { + TimiException.required(code, "not found code"); + GaoUserInviteCache cache = redisInvite.get(getInviteKey(code)); + if (cache == null || cache.getExpireAt() == null || cache.getExpireAt() <= Time.now()) { + throw invalidInvite(); + } + return cache; + } + + private GaoStore requireActiveStore(String storeId) { + GaoStore store = gaoStoreService.get(storeId); + if (store == null || !store.isActivated()) { + throw invalidInvite(); + } + return store; + } + + private Role requireInvitableRole(String inviterUserId, String roleId, String storeId) { + Role role = requireValidRole(roleId, storeId); + List directRoleIdList = userRoleService.listRoleByUserId(ModuleCode.GAO, inviterUserId) + .stream() + .map(Role::getId) + .toList(); + boolean descendant = authorizationScopeService.listManageableRole(inviterUserId, ModuleCode.GAO) + .stream() + .anyMatch(item -> role.getId().equals(item.getId())) + && !directRoleIdList.contains(role.getId()); + if (!descendant) { + throw new TimiException(TimiCode.PERMISSION_ERROR, "角色不在可邀请范围内"); + } + return role; + } + + private Role requireValidRole(String roleId, String storeId) { + Role role = roleService.get(roleId); + if (role == null || role.getModuleCode() != ModuleCode.GAO || !isRoleUsableForStore(role, storeId)) { + throw invalidInvite(); + } + return role; + } + + private boolean isRoleUsableForStore(Role role, String storeId) { + return role.getOwnerType() == null + || role.getOwnerType() == AuthOwnerType.MODULE && (TimiJava.isEmpty(role.getOwnerId()) || ModuleCode.GAO.name().equals(role.getOwnerId())) + || role.getOwnerType() == AuthOwnerType.STORE && storeId.equals(role.getOwnerId()); + } + + private void checkStoreScope(String userId, String storeId) { + if (authorizationScopeService.isSystemAdmin(userId) || roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) { + return; + } + GaoStore userStore = gaoStoreService.getByUserId(userId); + TimiException.requiredTrue(userStore != null && storeId.equals(userStore.getId()), "无权操作其他门店"); + } + + private String buildLogContent(GaoUserInviteCache cache) { + return "inviterUserId=%s;storeId=%s;roleId=%s;scene=%s".formatted(cache.getInviterUserId(), cache.getStoreId(), cache.getRoleId(), cache.getScene()); + } + + private void checkCodeRateLimit(String code) { + checkRateLimit("GAO:USER:INVITE:LIMIT:CODE:" + Digest.sha256(code), CODE_RATE_LIMIT, REGISTER_RATE_LIMIT_TTL); + } + + private void checkRegisterValueRateLimit(String type, String value) { + checkRateLimit("GAO:USER:INVITE:LIMIT:" + type + ":" + Digest.sha256(value), REGISTER_VALUE_RATE_LIMIT, REGISTER_RATE_LIMIT_TTL); + } + + private void checkRateLimit(String key, int limit, long ttl) { + String lockKey = key + ":LOCK"; + TimiException.requiredTrue(redisLocker.lock(lockKey, 1, Time.S * 5), "服务繁忙,请稍后重试"); + try { + Integer count = redisRateLimit.get(key); + if (count != null && limit <= count) { + throw new TimiException(TimiCode.REQUEST_BAD, "request_rate_limit"); + } + redisRateLimit.set(key, count == null ? 1 : count + 1, ttl); + } finally { + redisLocker.releaseLock(lockKey); + } + } + + private String getInviteKey(String code) { + return INVITE_KEY_PREFIX + code; + } + + private String getConsumeLockKey(String code) { + return CONSUME_LOCK_KEY_PREFIX + code; + } + + private TimiException invalidInvite() { + return new TimiException(TimiCode.ARG_BAD, "邀请码无效或已过期"); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java index 66891e4..44b42bd 100644 --- a/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/gao/service/implement/GaoUserServiceImplement.java @@ -1,6 +1,5 @@ package com.imyeyu.api.modules.gao.service.implement; -import com.fasterxml.jackson.databind.ObjectMapper; import com.imyeyu.api.config.dbsource.TimiServerDBConfig; import com.imyeyu.api.modules.gao.entity.GaoStore; import com.imyeyu.api.modules.gao.entity.GaoUser; @@ -30,7 +29,6 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class GaoUserServiceImplement extends AbstractEntityService implements GaoUserService { - private final ObjectMapper jackson; private final UserService userService; private final LoggerService loggerService; private final GaoStoreService gaoStoreService; @@ -52,7 +50,8 @@ public class GaoUserServiceImplement extends AbstractEntityService { + + /// 级联校验注册数据 + @Override + @NotNull + @Valid + public GaoUserInviteRegisterData getData() { + return super.getData(); + } +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateRequest.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateRequest.java new file mode 100644 index 0000000..b601a72 --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateRequest.java @@ -0,0 +1,18 @@ +package com.imyeyu.api.modules.gao.vo; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.Data; + +/// GAO 用户邀请校验请求 +/// +/// @author Codex +/// @since 2026-08-13 +@Data +public class GaoUserInviteValidateRequest { + + /// 邀请 code + @NotBlank + @Pattern(regexp = "[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{10}") + private String code; +} diff --git a/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateResponse.java b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateResponse.java new file mode 100644 index 0000000..e6197ae --- /dev/null +++ b/src/main/java/com/imyeyu/api/modules/gao/vo/GaoUserInviteValidateResponse.java @@ -0,0 +1,19 @@ +package com.imyeyu.api.modules.gao.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/// GAO 用户邀请校验响应 +/// +/// @author Codex +/// @since 2026-08-13 +@Data +@AllArgsConstructor +public class GaoUserInviteValidateResponse { + + /// true 为邀请码可用 + private boolean valid; + + /// 邀请过期时间,毫秒时间戳 + private Long expireAt; +} diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java index 5972d9a..d365d71 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserLoginServiceImplement.java @@ -18,6 +18,7 @@ import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.spring.TimiSpring; import com.imyeyu.spring.util.Redis; import com.imyeyu.utils.Digest; +import com.imyeyu.utils.Regex; import com.imyeyu.utils.Time; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -38,7 +39,6 @@ import java.util.concurrent.ThreadLocalRandom; @RequiredArgsConstructor(onConstructor_ = {@Lazy}) public class UserLoginServiceImplement implements UserLoginService, TimiJava { - private static final String REGEX_PHONE_NO = "^1\\d{10}$"; private static final String USER_TOKEN_KEY_PREFIX = "USER:TOKEN:"; private static final String PHONE_VERIFY_KEY_PREFIX = "USER:PHONE:VERIFY:"; private static final long PHONE_VERIFY_CODE_TTL = Time.M * 5; @@ -61,7 +61,7 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava { User dbUser = userService.get(req.getUser()); if (dbUser == null && req.getUser().contains("@")) { dbUser = userService.getByVerifiedEmail(req.getUser()); - } else if (dbUser == null && isPhoneNo(req.getUser())) { + } else if (dbUser == null && Regex.isMatch(Regex.MOBILE_PHONE, req.getUser())) { dbUser = userService.getByVerifiedPhoneNo(req.getUser()); } else if (dbUser == null) { dbUser = userService.getByName(req.getUser()); @@ -143,8 +143,8 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava { @Transactional(TimiServerDBConfig.ROLLBACKER) @Override public void sendPhoneVerifyCode(String phoneNo) { - TimiException.requiredTrue(isPhoneNo(phoneNo), "invalid phone number"); - User verifiedUser = userService.getByVerifiedPhoneNo(phoneNo); + TimiException.requiredTrue(Regex.isMatch(Regex.MOBILE_PHONE, phoneNo), "invalid phone number"); + User verifiedUser = userService.getByPhoneNo(phoneNo); User loginUser = getRequireLoginUser(); TimiException.requiredTrue(verifiedUser == null || verifiedUser.getId().equals(loginUser.getId()), "existed phone number"); @@ -156,11 +156,11 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava { @Transactional(TimiServerDBConfig.ROLLBACKER) @Override public void verifyPhoneNo(String phoneNo, String verifyCode) { - TimiException.requiredTrue(isPhoneNo(phoneNo), "invalid phone number"); + TimiException.requiredTrue(Regex.isMatch(Regex.MOBILE_PHONE, phoneNo), "invalid phone number"); TimiException.required(verifyCode, "not found verify code"); User user = getRequireLoginUser(); - User verifiedUser = userService.getByVerifiedPhoneNo(phoneNo); + User verifiedUser = userService.getByPhoneNo(phoneNo); TimiException.requiredTrue(verifiedUser == null || verifiedUser.getId().equals(user.getId()), "existed phone number"); String cacheCode = redisUserPhoneVerify.get(getPhoneVerifyKey(phoneNo)); @@ -188,10 +188,6 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava { userService.delete(user.getId()); } - private boolean isPhoneNo(String value) { - return TimiJava.isNotEmpty(value) && value.matches(REGEX_PHONE_NO); - } - private String getPhoneVerifyKey(String phoneNo) { return "%s%s".formatted(PHONE_VERIFY_KEY_PREFIX, phoneNo); } diff --git a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java index 0f0ba8f..9274af4 100644 --- a/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java +++ b/src/main/java/com/imyeyu/api/modules/user/service/implement/UserServiceImplement.java @@ -57,7 +57,7 @@ public class UserServiceImplement extends AbstractEntityService im { if (TimiJava.isNotEmpty(user.getPhoneNo())) { TimiException.requiredTrue(user.isValidPhone(), "invalid phone"); - User byPhoneNo = getByVerifiedPhoneNo(user.getPhoneNo()); + User byPhoneNo = getByPhoneNo(user.getPhoneNo()); TimiException.requiredNull(byPhoneNo, "existed phone number"); } } @@ -94,7 +94,7 @@ public class UserServiceImplement extends AbstractEntityService im if (TimiJava.isNotEmpty(phoneNo)) { TimiException.requiredTrue(user.isValidPhone(), "invalid phone"); if (!phoneNo.equals(dbUser.getPhoneNo())) { - User byPhoneNo = getByVerifiedPhoneNo(phoneNo); + User byPhoneNo = getByPhoneNo(phoneNo); TimiException.requiredTrue(byPhoneNo == null || byPhoneNo.getId().equals(dbUser.getId()), "existed phone number"); } }