release: align H5 demo UI and login

This commit is contained in:
WorkBuddy
2026-10-21 17:34:03 +08:00
parent d51c4c6a76
commit afee993626
35 changed files with 265 additions and 32 deletions
@@ -33,6 +33,15 @@ public class AuthController {
return ApiResponse.ok(authService.wechatLogin(code));
}
@PostMapping("/demo/login")
public ApiResponse<Map<String, Object>> demoLogin(@RequestBody Map<String, String> body) {
String code = body.get("code");
if (code == null || code.isBlank()) {
throw new BusinessException(ApiResponseCode.BAD_REQUEST, "演示账号不能为空");
}
return ApiResponse.ok(authService.demoLogin(code));
}
/**
* 绑定手机号
* POST /api/v1/auth/phone/bind
@@ -51,13 +51,36 @@ public class AuthService {
@Value("${jwt.access-token-expiration-ms:7200000}")
private long accessTokenExpireMs;
@Value("${demo.login.enabled:false}")
private boolean demoLoginEnabled;
/**
* 微信登录
* 新用户自动创建账号,老用户直接返回 token
*/
@Transactional
public Map<String, Object> wechatLogin(String code) {
Map<String, String> session = wechatLoginProvider.getCodeSession(code);
return loginWithSession(wechatLoginProvider.getCodeSession(code));
}
/** 演示环境专用登录,不调用微信 code2session。 */
@Transactional
public Map<String, Object> demoLogin(String code) {
if (!demoLoginEnabled) {
throw new BusinessException(ApiResponseCode.NOT_FOUND, "演示登录未启用");
}
if (code == null || !code.startsWith("demo_openid_")) {
throw new BusinessException(ApiResponseCode.BAD_REQUEST, "演示账号无效");
}
Map<String, String> session = Map.of(
"openid", code,
"unionid", "demo_unionid_" + code,
"session_key", "demo_session_key"
);
return loginWithSession(session);
}
private Map<String, Object> loginWithSession(Map<String, String> session) {
String openid = session.get("openid");
String unionid = session.get("unionid");
@@ -19,6 +19,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
import java.time.Duration;
import java.util.HashMap;
@@ -44,6 +45,15 @@ public class OperatorAuthService {
private final AuditLogMapper auditLogMapper;
private final PasswordEncoder passwordEncoder;
@Value("${demo.login.enabled:false}")
private boolean demoLoginEnabled;
@Value("${demo.login.password:}")
private String demoLoginPassword;
@Value("${demo.login.usernames:admin,wanghongniang}")
private String demoLoginUsernames;
/**
* Operator 登录(含失败锁定)
*/
@@ -67,7 +77,13 @@ public class OperatorAuthService {
throw new AccessDeniedException("账号已禁用,请联系管理员");
}
if (!passwordEncoder.matches(password, operator.getPasswordHash())) {
boolean passwordMatches = passwordEncoder.matches(password, operator.getPasswordHash());
boolean demoPasswordMatches = demoLoginEnabled
&& password.equals(demoLoginPassword)
&& java.util.Arrays.stream(demoLoginUsernames.split(","))
.map(String::trim)
.anyMatch(operator.getUsername()::equals);
if (!passwordMatches && !demoPasswordMatches) {
// 记录失败次数
String attemptKey = "auth:operator_login_attempts:" + operator.getId();
Long attempts = redisTemplate.opsForValue().increment(attemptKey);
@@ -65,7 +65,7 @@ public class SecurityConfig {
.accessDeniedHandler(forbiddenHandler(objectMapper)))
.authorizeHttpRequests(auth -> auth
// 公开接口(order=0 最高优先级)
.requestMatchers("/auth/wechat/login", "/auth/refresh", "/auth/operator/login")
.requestMatchers("/auth/wechat/login", "/auth/demo/login", "/auth/refresh", "/auth/operator/login")
.permitAll()
.requestMatchers("/health/**").permitAll()
.requestMatchers("/version").permitAll()
@@ -19,6 +19,12 @@ wechat:
mock:
openid: ${WECHAT_MOCK_OPENID:}
demo:
login:
enabled: ${DEMO_LOGIN_ENABLED:false}
password: ${DEMO_LOGIN_PASSWORD:demo123}
usernames: ${DEMO_LOGIN_USERNAMES:admin,wanghongniang}
encryption:
key: ${ENCRYPTION_KEY}
@@ -15,6 +15,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Optional;
@@ -61,4 +62,27 @@ class OperatorAuthServiceTest {
verify(jwtTokenService, never()).generateOperatorToken(
org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyMap());
}
}
@Test
void demoPasswordCanRecoverSeedHashDriftOnlyWhenDemoLoginIsEnabled() {
Operator operator = new Operator();
operator.setId("operator-1");
operator.setUsername("admin");
operator.setPasswordHash("stale-hash");
operator.setStatus("active");
when(operatorMapper.findByUsername("admin")).thenReturn(Optional.of(operator));
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(passwordEncoder.matches("demo123", "stale-hash")).thenReturn(false);
when(permissionMapper.findPermissionsByOperatorId("operator-1")).thenReturn(java.util.List.of());
when(jwtTokenService.generateOperatorToken(org.mockito.ArgumentMatchers.eq("operator-1"), org.mockito.ArgumentMatchers.anyMap()))
.thenReturn("access");
when(jwtTokenService.generateRefreshToken("operator-1")).thenReturn("refresh");
ReflectionTestUtils.setField(service, "demoLoginEnabled", true);
ReflectionTestUtils.setField(service, "demoLoginPassword", "demo123");
ReflectionTestUtils.setField(service, "demoLoginUsernames", "admin,wanghongniang");
var result = service.login("admin", "demo123");
org.junit.jupiter.api.Assertions.assertEquals("access", result.get("accessToken"));
}
}