Commit 910a5ba7c504f2acf205aea6c01b310e774f78f5

Authored by zichun
2 parents 62aeb80f 07c91f41

Merge branch 'module-module_usr'

Showing 102 changed files with 11131 additions and 11 deletions
backend/.mvn/jvm.config 0 → 100644
  1 +-XX:+EnableDynamicAgentLoading
... ...
backend/checkstyle.xml 0 → 100644
  1 +<?xml version="1.0"?>
  2 +<!DOCTYPE module PUBLIC
  3 + "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
  4 + "https://checkstyle.org/dtds/configuration_1_3.dtd">
  5 +<!--
  6 + 项目 checkstyle 规则(Spring Boot + Lombok 友好,不使用 sun_checks.xml 的过严规则)
  7 + 规则原则:检查明显的代码问题,不干预 Lombok 注解生成类、不要求 final 参数、不限制行长。
  8 +-->
  9 +<module name="Checker">
  10 + <property name="charset" value="UTF-8"/>
  11 + <property name="severity" value="error"/>
  12 + <property name="fileExtensions" value="java"/>
  13 +
  14 + <module name="TreeWalker">
  15 + <!-- 未使用的 import -->
  16 + <module name="UnusedImports"/>
  17 + <!-- 重复 import -->
  18 + <module name="RedundantImport"/>
  19 + <!-- 通配符 import(java.util.* 等)-->
  20 + <module name="AvoidStarImport"/>
  21 +
  22 + <!-- 左大括号位置 -->
  23 + <module name="LeftCurly"/>
  24 + <!-- 空代码块(空 catch 需有注释)-->
  25 + <module name="EmptyBlock">
  26 + <property name="option" value="text"/>
  27 + </module>
  28 +
  29 + <!-- switch 缺少 default -->
  30 + <module name="MissingSwitchDefault"/>
  31 +
  32 + <!-- == 比较 String 字面量 -->
  33 + <module name="StringLiteralEquality"/>
  34 +
  35 + <!-- 多余的分号 -->
  36 + <module name="EmptyStatement"/>
  37 +
  38 + <!-- 修饰符顺序 (public static final ...) -->
  39 + <module name="ModifierOrder"/>
  40 +
  41 + <!-- 不允许 System.out.println(生产代码用 logger)-->
  42 + <module name="Regexp">
  43 + <property name="format" value="System\.(out|err)\.print"/>
  44 + <property name="illegalPattern" value="true"/>
  45 + <property name="message" value="请使用 logger 而非 System.out/err.print"/>
  46 + </module>
  47 + </module>
  48 +</module>
... ...
backend/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <modelVersion>4.0.0</modelVersion>
  6 +
  7 + <parent>
  8 + <groupId>org.springframework.boot</groupId>
  9 + <artifactId>spring-boot-starter-parent</artifactId>
  10 + <version>3.3.5</version>
  11 + <relativePath/>
  12 + </parent>
  13 +
  14 + <groupId>com.example</groupId>
  15 + <artifactId>erp</artifactId>
  16 + <version>0.0.1-SNAPSHOT</version>
  17 + <name>erp</name>
  18 + <description>小羚羊 ERP 后端</description>
  19 + <packaging>jar</packaging>
  20 +
  21 + <properties>
  22 + <java.version>21</java.version>
  23 + <lombok.version>1.18.36</lombok.version>
  24 + <mybatis-plus.version>3.5.7</mybatis-plus.version>
  25 + <jjwt.version>0.12.6</jjwt.version>
  26 + <hutool.version>5.8.28</hutool.version>
  27 + <flyway.version>10.17.0</flyway.version>
  28 + </properties>
  29 +
  30 + <dependencies>
  31 + <!-- Web -->
  32 + <dependency>
  33 + <groupId>org.springframework.boot</groupId>
  34 + <artifactId>spring-boot-starter-web</artifactId>
  35 + </dependency>
  36 +
  37 + <!-- Security -->
  38 + <dependency>
  39 + <groupId>org.springframework.boot</groupId>
  40 + <artifactId>spring-boot-starter-security</artifactId>
  41 + </dependency>
  42 +
  43 + <!-- Validation -->
  44 + <dependency>
  45 + <groupId>org.springframework.boot</groupId>
  46 + <artifactId>spring-boot-starter-validation</artifactId>
  47 + </dependency>
  48 +
  49 + <!-- MyBatis-Plus (Spring Boot 3) -->
  50 + <dependency>
  51 + <groupId>com.baomidou</groupId>
  52 + <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
  53 + <version>${mybatis-plus.version}</version>
  54 + </dependency>
  55 +
  56 + <!-- MySQL -->
  57 + <dependency>
  58 + <groupId>com.mysql</groupId>
  59 + <artifactId>mysql-connector-j</artifactId>
  60 + <scope>runtime</scope>
  61 + </dependency>
  62 +
  63 + <!-- Flyway -->
  64 + <dependency>
  65 + <groupId>org.flywaydb</groupId>
  66 + <artifactId>flyway-core</artifactId>
  67 + <version>${flyway.version}</version>
  68 + </dependency>
  69 + <dependency>
  70 + <groupId>org.flywaydb</groupId>
  71 + <artifactId>flyway-mysql</artifactId>
  72 + <version>${flyway.version}</version>
  73 + </dependency>
  74 +
  75 + <!-- JWT (JJWT 0.12.x) -->
  76 + <dependency>
  77 + <groupId>io.jsonwebtoken</groupId>
  78 + <artifactId>jjwt-api</artifactId>
  79 + <version>${jjwt.version}</version>
  80 + </dependency>
  81 + <dependency>
  82 + <groupId>io.jsonwebtoken</groupId>
  83 + <artifactId>jjwt-impl</artifactId>
  84 + <version>${jjwt.version}</version>
  85 + <scope>runtime</scope>
  86 + </dependency>
  87 + <dependency>
  88 + <groupId>io.jsonwebtoken</groupId>
  89 + <artifactId>jjwt-jackson</artifactId>
  90 + <version>${jjwt.version}</version>
  91 + <scope>runtime</scope>
  92 + </dependency>
  93 +
  94 + <!-- Lombok -->
  95 + <dependency>
  96 + <groupId>org.projectlombok</groupId>
  97 + <artifactId>lombok</artifactId>
  98 + <optional>true</optional>
  99 + </dependency>
  100 +
  101 + <!-- Hutool -->
  102 + <dependency>
  103 + <groupId>cn.hutool</groupId>
  104 + <artifactId>hutool-all</artifactId>
  105 + <version>${hutool.version}</version>
  106 + </dependency>
  107 +
  108 + <!-- Test -->
  109 + <dependency>
  110 + <groupId>org.springframework.boot</groupId>
  111 + <artifactId>spring-boot-starter-test</artifactId>
  112 + <scope>test</scope>
  113 + </dependency>
  114 + <dependency>
  115 + <groupId>org.springframework.security</groupId>
  116 + <artifactId>spring-security-test</artifactId>
  117 + <scope>test</scope>
  118 + </dependency>
  119 + </dependencies>
  120 +
  121 + <build>
  122 + <plugins>
  123 + <plugin>
  124 + <groupId>org.apache.maven.plugins</groupId>
  125 + <artifactId>maven-checkstyle-plugin</artifactId>
  126 + <configuration>
  127 + <configLocation>checkstyle.xml</configLocation>
  128 + <consoleOutput>true</consoleOutput>
  129 + <failsOnError>true</failsOnError>
  130 + <includeTestSourceDirectory>false</includeTestSourceDirectory>
  131 + </configuration>
  132 + </plugin>
  133 + <plugin>
  134 + <groupId>org.apache.maven.plugins</groupId>
  135 + <artifactId>maven-surefire-plugin</artifactId>
  136 + <configuration>
  137 + <argLine>
  138 + -XX:+EnableDynamicAgentLoading
  139 + -Dnet.bytebuddy.experimental=true
  140 + --add-opens java.base/java.lang=ALL-UNNAMED
  141 + --add-opens java.base/java.lang.invoke=ALL-UNNAMED
  142 + --add-opens java.base/java.util=ALL-UNNAMED
  143 + </argLine>
  144 + </configuration>
  145 + </plugin>
  146 + <plugin>
  147 + <groupId>org.springframework.boot</groupId>
  148 + <artifactId>spring-boot-maven-plugin</artifactId>
  149 + <configuration>
  150 + <excludes>
  151 + <exclude>
  152 + <groupId>org.projectlombok</groupId>
  153 + <artifactId>lombok</artifactId>
  154 + </exclude>
  155 + </excludes>
  156 + </configuration>
  157 + </plugin>
  158 + </plugins>
  159 + </build>
  160 +</project>
... ...
backend/src/main/java/com/example/erp/Application.java 0 → 100644
  1 +package com.example.erp;
  2 +
  3 +import org.springframework.boot.SpringApplication;
  4 +import org.springframework.boot.autoconfigure.SpringBootApplication;
  5 +
  6 +@SpringBootApplication
  7 +public class Application {
  8 + public static void main(String[] args) {
  9 + SpringApplication.run(Application.class, args);
  10 + }
  11 +}
... ...
backend/src/main/java/com/example/erp/common/constants/AuthErrorCode.java 0 → 100644
  1 +package com.example.erp.common.constants;
  2 +
  3 +public final class AuthErrorCode {
  4 +
  5 + public static final int USERNAME_OR_PASSWORD_ERROR = 40100;
  6 + public static final int ACCOUNT_DISABLED = 40101;
  7 + public static final int ACCOUNT_LOCKED = 40102;
  8 + public static final int REFRESH_TOKEN_INVALID = 40103;
  9 +
  10 + private AuthErrorCode() {}
  11 +}
... ...
backend/src/main/java/com/example/erp/common/constants/UsrErrorCode.java 0 → 100644
  1 +package com.example.erp.common.constants;
  2 +
  3 +public final class UsrErrorCode {
  4 +
  5 + public static final int PERMISSION_DENIED = 40300;
  6 + public static final int SELF_ADMIN_CHANGE = 40301;
  7 + public static final int USERNAME_EXISTS = 40901;
  8 + public static final int USER_CODE_EXISTS = 40902;
  9 + public static final int EMPLOYEE_NOT_FOUND = 40001;
  10 + public static final int USER_NOT_FOUND = 40400;
  11 +
  12 + private UsrErrorCode() {}
  13 +}
... ...
backend/src/main/java/com/example/erp/common/exception/BizException.java 0 → 100644
  1 +package com.example.erp.common.exception;
  2 +
  3 +import lombok.Getter;
  4 +
  5 +@Getter
  6 +public class BizException extends RuntimeException {
  7 +
  8 + private final int code;
  9 +
  10 + public BizException(int code, String message) {
  11 + super(message);
  12 + this.code = code;
  13 + }
  14 +}
... ...
backend/src/main/java/com/example/erp/common/exception/GlobalExceptionHandler.java 0 → 100644
  1 +package com.example.erp.common.exception;
  2 +
  3 +import com.example.erp.common.response.Result;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.validation.FieldError;
  6 +import org.springframework.web.bind.MethodArgumentNotValidException;
  7 +import org.springframework.web.bind.annotation.ExceptionHandler;
  8 +import org.springframework.web.bind.annotation.RestControllerAdvice;
  9 +
  10 +@Slf4j
  11 +@RestControllerAdvice
  12 +public class GlobalExceptionHandler {
  13 +
  14 + @ExceptionHandler(BizException.class)
  15 + public Result<Void> handleBizException(BizException e) {
  16 + return Result.fail(e.getCode(), e.getMessage());
  17 + }
  18 +
  19 + @ExceptionHandler(MethodArgumentNotValidException.class)
  20 + public Result<Void> handleValidation(MethodArgumentNotValidException e) {
  21 + FieldError fe = e.getBindingResult().getFieldErrors().stream().findFirst().orElse(null);
  22 + String msg = fe != null ? fe.getDefaultMessage() : "参数校验失败";
  23 + return Result.fail(40001, msg);
  24 + }
  25 +
  26 + @ExceptionHandler(Exception.class)
  27 + public Result<Void> handleException(Exception e) {
  28 + log.error("未处理异常", e);
  29 + return Result.fail(99000, "系统内部错误");
  30 + }
  31 +}
... ...
backend/src/main/java/com/example/erp/common/response/Result.java 0 → 100644
  1 +package com.example.erp.common.response;
  2 +
  3 +import lombok.Getter;
  4 +
  5 +@Getter
  6 +public class Result<T> {
  7 +
  8 + private final int code;
  9 + private final String message;
  10 + private final T data;
  11 + private final long timestamp;
  12 +
  13 + private Result(int code, String message, T data) {
  14 + this.code = code;
  15 + this.message = message;
  16 + this.data = data;
  17 + this.timestamp = System.currentTimeMillis();
  18 + }
  19 +
  20 + public static <T> Result<T> ok(T data) {
  21 + return new Result<>(200, "操作成功", data);
  22 + }
  23 +
  24 + public static <T> Result<T> fail(int code, String message) {
  25 + return new Result<>(code, message, null);
  26 + }
  27 +}
... ...
backend/src/main/java/com/example/erp/common/util/JwtUtil.java 0 → 100644
  1 +package com.example.erp.common.util;
  2 +
  3 +import com.example.erp.common.constants.AuthErrorCode;
  4 +import com.example.erp.common.exception.BizException;
  5 +import com.example.erp.config.JwtProperties;
  6 +import io.jsonwebtoken.Claims;
  7 +import io.jsonwebtoken.JwtException;
  8 +import io.jsonwebtoken.Jwts;
  9 +import io.jsonwebtoken.security.Keys;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.stereotype.Component;
  12 +
  13 +import javax.crypto.SecretKey;
  14 +import java.nio.charset.StandardCharsets;
  15 +import java.util.Date;
  16 +
  17 +@Component
  18 +@RequiredArgsConstructor
  19 +public class JwtUtil {
  20 +
  21 + private final JwtProperties properties;
  22 +
  23 + private SecretKey key() {
  24 + return Keys.hmacShaKeyFor(properties.getSecret().getBytes(StandardCharsets.UTF_8));
  25 + }
  26 +
  27 + public String generateAccessToken(String userId, String username, String userType, String brandId) {
  28 + long now = System.currentTimeMillis();
  29 + return Jwts.builder()
  30 + .subject(userId)
  31 + .claim("username", username)
  32 + .claim("userType", userType)
  33 + .claim("brandId", brandId)
  34 + .issuedAt(new Date(now))
  35 + .expiration(new Date(now + properties.getAccessTokenExpiry() * 1000))
  36 + .signWith(key(), Jwts.SIG.HS256)
  37 + .compact();
  38 + }
  39 +
  40 + public String generateRefreshToken(String userId, String brandId) {
  41 + long now = System.currentTimeMillis();
  42 + return Jwts.builder()
  43 + .subject(userId)
  44 + .claim("brandId", brandId)
  45 + .claim("type", "refresh")
  46 + .issuedAt(new Date(now))
  47 + .expiration(new Date(now + properties.getRefreshTokenExpiry() * 1000))
  48 + .signWith(key(), Jwts.SIG.HS256)
  49 + .compact();
  50 + }
  51 +
  52 + public Claims parseAccessToken(String token) {
  53 + return doParse(token);
  54 + }
  55 +
  56 + public Claims parseRefreshToken(String token) {
  57 + Claims claims = doParse(token);
  58 + if (!"refresh".equals(claims.get("type", String.class))) {
  59 + throw new BizException(AuthErrorCode.REFRESH_TOKEN_INVALID, "Refresh Token 已失效,请重新登录");
  60 + }
  61 + return claims;
  62 + }
  63 +
  64 + private Claims doParse(String token) {
  65 + try {
  66 + return Jwts.parser()
  67 + .verifyWith(key())
  68 + .build()
  69 + .parseSignedClaims(token)
  70 + .getPayload();
  71 + } catch (JwtException e) {
  72 + throw new BizException(AuthErrorCode.REFRESH_TOKEN_INVALID, "Token 已失效,请重新登录");
  73 + }
  74 + }
  75 +}
... ...
backend/src/main/java/com/example/erp/common/vo/PageVO.java 0 → 100644
  1 +package com.example.erp.common.vo;
  2 +
  3 +import com.baomidou.mybatisplus.core.metadata.IPage;
  4 +import lombok.Getter;
  5 +import lombok.Setter;
  6 +
  7 +import java.util.List;
  8 +
  9 +@Getter
  10 +@Setter
  11 +public class PageVO<T> {
  12 +
  13 + private long total;
  14 + private long page;
  15 + private long pageSize;
  16 + private List<T> list;
  17 +
  18 + public static <T> PageVO<T> of(IPage<T> iPage) {
  19 + PageVO<T> vo = new PageVO<>();
  20 + vo.total = iPage.getTotal();
  21 + vo.page = iPage.getCurrent();
  22 + vo.pageSize = iPage.getSize();
  23 + vo.list = iPage.getRecords();
  24 + return vo;
  25 + }
  26 +}
... ...
backend/src/main/java/com/example/erp/config/BeanConfig.java 0 → 100644
  1 +package com.example.erp.config;
  2 +
  3 +import org.springframework.context.annotation.Bean;
  4 +import org.springframework.context.annotation.Configuration;
  5 +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  6 +
  7 +@Configuration
  8 +public class BeanConfig {
  9 +
  10 + @Bean
  11 + public BCryptPasswordEncoder passwordEncoder() {
  12 + return new BCryptPasswordEncoder();
  13 + }
  14 +}
... ...
backend/src/main/java/com/example/erp/config/JwtAuthenticationFilter.java 0 → 100644
  1 +package com.example.erp.config;
  2 +
  3 +import com.example.erp.common.exception.BizException;
  4 +import com.example.erp.common.util.JwtUtil;
  5 +import io.jsonwebtoken.Claims;
  6 +import jakarta.servlet.FilterChain;
  7 +import jakarta.servlet.ServletException;
  8 +import jakarta.servlet.http.HttpServletRequest;
  9 +import jakarta.servlet.http.HttpServletResponse;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
  12 +import org.springframework.security.core.context.SecurityContextHolder;
  13 +import org.springframework.stereotype.Component;
  14 +import org.springframework.util.StringUtils;
  15 +import org.springframework.web.filter.OncePerRequestFilter;
  16 +
  17 +import java.io.IOException;
  18 +import java.util.Collections;
  19 +
  20 +@Component
  21 +@RequiredArgsConstructor
  22 +public class JwtAuthenticationFilter extends OncePerRequestFilter {
  23 +
  24 + private final JwtUtil jwtUtil;
  25 +
  26 + @Override
  27 + protected void doFilterInternal(HttpServletRequest request,
  28 + HttpServletResponse response,
  29 + FilterChain chain) throws ServletException, IOException {
  30 + String header = request.getHeader("Authorization");
  31 + if (StringUtils.hasText(header) && header.startsWith("Bearer ")) {
  32 + String token = header.substring(7);
  33 + try {
  34 + Claims claims = jwtUtil.parseAccessToken(token);
  35 + UserPrincipal principal = new UserPrincipal(
  36 + claims.getSubject(),
  37 + claims.get("username", String.class),
  38 + claims.get("userType", String.class),
  39 + claims.get("brandId", String.class));
  40 + UsernamePasswordAuthenticationToken auth =
  41 + new UsernamePasswordAuthenticationToken(
  42 + principal, null, Collections.emptyList());
  43 + SecurityContextHolder.getContext().setAuthentication(auth);
  44 + } catch (BizException ignored) {
  45 + // invalid token — no auth set; Spring Security will return 401
  46 + }
  47 + }
  48 + chain.doFilter(request, response);
  49 + }
  50 +}
... ...
backend/src/main/java/com/example/erp/config/JwtProperties.java 0 → 100644
  1 +package com.example.erp.config;
  2 +
  3 +import lombok.Getter;
  4 +import lombok.Setter;
  5 +import org.springframework.boot.context.properties.ConfigurationProperties;
  6 +import org.springframework.stereotype.Component;
  7 +
  8 +@Getter
  9 +@Setter
  10 +@Component
  11 +@ConfigurationProperties(prefix = "jwt")
  12 +public class JwtProperties {
  13 + private String secret;
  14 + private long accessTokenExpiry;
  15 + private long refreshTokenExpiry;
  16 +}
... ...
backend/src/main/java/com/example/erp/config/MyBatisPlusConfig.java 0 → 100644
  1 +package com.example.erp.config;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.DbType;
  4 +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
  5 +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
  6 +import org.mybatis.spring.annotation.MapperScan;
  7 +import org.springframework.context.annotation.Bean;
  8 +import org.springframework.context.annotation.Configuration;
  9 +
  10 +@Configuration
  11 +@MapperScan("com.example.erp.module.*.mapper")
  12 +public class MyBatisPlusConfig {
  13 +
  14 + @Bean
  15 + public MybatisPlusInterceptor mybatisPlusInterceptor() {
  16 + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  17 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
  18 + return interceptor;
  19 + }
  20 +}
... ...
backend/src/main/java/com/example/erp/config/SecurityConfig.java 0 → 100644
  1 +package com.example.erp.config;
  2 +
  3 +import jakarta.servlet.http.HttpServletResponse;
  4 +import lombok.RequiredArgsConstructor;
  5 +import org.springframework.context.annotation.Bean;
  6 +import org.springframework.context.annotation.Configuration;
  7 +import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  8 +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  9 +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
  10 +import org.springframework.security.config.http.SessionCreationPolicy;
  11 +import org.springframework.security.web.SecurityFilterChain;
  12 +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
  13 +
  14 +@Configuration
  15 +@EnableWebSecurity
  16 +@RequiredArgsConstructor
  17 +public class SecurityConfig {
  18 +
  19 + private final JwtAuthenticationFilter jwtAuthenticationFilter;
  20 +
  21 + @Bean
  22 + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  23 + http
  24 + .csrf(AbstractHttpConfigurer::disable)
  25 + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
  26 + .authorizeHttpRequests(auth -> auth
  27 + .requestMatchers("/api/auth/**").permitAll()
  28 + .anyRequest().authenticated()
  29 + )
  30 + .exceptionHandling(ex -> ex
  31 + .authenticationEntryPoint((request, response, authException) ->
  32 + response.sendError(HttpServletResponse.SC_UNAUTHORIZED)))
  33 + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
  34 + return http.build();
  35 + }
  36 +}
... ...
backend/src/main/java/com/example/erp/config/UserPrincipal.java 0 → 100644
  1 +package com.example.erp.config;
  2 +
  3 +public record UserPrincipal(String userId, String username, String userType, String brandId) {}
... ...
backend/src/main/java/com/example/erp/module/usr/controller/AuthController.java 0 → 100644
  1 +package com.example.erp.module.usr.controller;
  2 +
  3 +import com.example.erp.common.response.Result;
  4 +import com.example.erp.module.usr.dto.LoginReqDTO;
  5 +import com.example.erp.module.usr.dto.RefreshTokenReqDTO;
  6 +import com.example.erp.module.usr.service.AuthService;
  7 +import com.example.erp.module.usr.vo.BrandVO;
  8 +import com.example.erp.module.usr.vo.LoginVO;
  9 +import jakarta.validation.Valid;
  10 +import lombok.RequiredArgsConstructor;
  11 +import org.springframework.web.bind.annotation.GetMapping;
  12 +import org.springframework.web.bind.annotation.PostMapping;
  13 +import org.springframework.web.bind.annotation.RequestBody;
  14 +import org.springframework.web.bind.annotation.RequestMapping;
  15 +import org.springframework.web.bind.annotation.RestController;
  16 +
  17 +import java.util.List;
  18 +import java.util.Map;
  19 +
  20 +@RestController
  21 +@RequestMapping("/api/auth")
  22 +@RequiredArgsConstructor
  23 +public class AuthController {
  24 +
  25 + private final AuthService authService;
  26 +
  27 + @PostMapping("/login")
  28 + public Result<LoginVO> login(@Valid @RequestBody LoginReqDTO req) {
  29 + return Result.ok(authService.login(req));
  30 + }
  31 +
  32 + @PostMapping("/refresh")
  33 + public Result<Map<String, String>> refresh(@Valid @RequestBody RefreshTokenReqDTO req) {
  34 + String newToken = authService.refresh(req.getRefreshToken());
  35 + return Result.ok(Map.of("accessToken", newToken));
  36 + }
  37 +
  38 + @GetMapping("/brands")
  39 + public Result<List<BrandVO>> brands() {
  40 + return Result.ok(authService.getBrands());
  41 + }
  42 +}
... ...
backend/src/main/java/com/example/erp/module/usr/controller/UserController.java 0 → 100644
  1 +package com.example.erp.module.usr.controller;
  2 +
  3 +import com.example.erp.common.response.Result;
  4 +import com.example.erp.common.vo.PageVO;
  5 +import com.example.erp.config.UserPrincipal;
  6 +import com.example.erp.module.usr.dto.UserCreateReqDTO;
  7 +import com.example.erp.module.usr.dto.UserListQueryDTO;
  8 +import com.example.erp.module.usr.dto.UserUpdateReqDTO;
  9 +import com.example.erp.module.usr.service.UserService;
  10 +import com.example.erp.module.usr.vo.PermissionGroupVO;
  11 +import com.example.erp.module.usr.vo.StaffVO;
  12 +import com.example.erp.module.usr.vo.UserCreateRespVO;
  13 +import com.example.erp.module.usr.vo.UserListItemVO;
  14 +import com.example.erp.module.usr.vo.UserUpdateRespVO;
  15 +import jakarta.validation.Valid;
  16 +import lombok.RequiredArgsConstructor;
  17 +import org.springframework.security.core.annotation.AuthenticationPrincipal;
  18 +import org.springframework.web.bind.annotation.GetMapping;
  19 +import org.springframework.web.bind.annotation.PathVariable;
  20 +import org.springframework.web.bind.annotation.PostMapping;
  21 +import org.springframework.web.bind.annotation.PutMapping;
  22 +import org.springframework.web.bind.annotation.RequestBody;
  23 +import org.springframework.web.bind.annotation.RequestMapping;
  24 +import org.springframework.web.bind.annotation.RequestParam;
  25 +import org.springframework.web.bind.annotation.RestController;
  26 +
  27 +import java.util.List;
  28 +
  29 +@RestController
  30 +@RequestMapping("/api/usr")
  31 +@RequiredArgsConstructor
  32 +public class UserController {
  33 +
  34 + private final UserService userService;
  35 +
  36 + @PostMapping("/users")
  37 + public Result<UserCreateRespVO> createUser(
  38 + @Valid @RequestBody UserCreateReqDTO req,
  39 + @AuthenticationPrincipal UserPrincipal principal) {
  40 + return Result.ok(userService.createUser(req, principal));
  41 + }
  42 +
  43 + // REQ-USR-003: 查询用户
  44 + @GetMapping("/users")
  45 + public Result<PageVO<UserListItemVO>> getUsers(
  46 + @RequestParam(defaultValue = "username") String queryField,
  47 + @RequestParam(defaultValue = "contains") String matchType,
  48 + @RequestParam(defaultValue = "") String queryValue,
  49 + @RequestParam(defaultValue = "1") int page,
  50 + @RequestParam(defaultValue = "20") int pageSize,
  51 + @AuthenticationPrincipal UserPrincipal principal) {
  52 + UserListQueryDTO q = new UserListQueryDTO();
  53 + q.setQueryField(queryField);
  54 + q.setMatchType(matchType);
  55 + q.setQueryValue(queryValue);
  56 + q.setPage(page);
  57 + q.setPageSize(pageSize);
  58 + return Result.ok(userService.getUserList(q, principal.brandId()));
  59 + }
  60 +
  61 + // REQ-USR-002: 修改用户
  62 + @PutMapping("/users/{userId}")
  63 + public Result<UserUpdateRespVO> updateUser(
  64 + @PathVariable String userId,
  65 + @Valid @RequestBody UserUpdateReqDTO req,
  66 + @AuthenticationPrincipal UserPrincipal principal) {
  67 + return Result.ok(userService.updateUser(userId, req, principal));
  68 + }
  69 +
  70 + @GetMapping("/users/staffs")
  71 + public Result<List<StaffVO>> getStaffs(@AuthenticationPrincipal UserPrincipal principal) {
  72 + return Result.ok(userService.getStaffs(principal.brandId()));
  73 + }
  74 +
  75 + @GetMapping("/users/permission-groups")
  76 + public Result<List<PermissionGroupVO>> getPermissionGroups(
  77 + @AuthenticationPrincipal UserPrincipal principal) {
  78 + return Result.ok(userService.getPermissionGroups(principal.brandId()));
  79 + }
  80 +}
... ...
backend/src/main/java/com/example/erp/module/usr/dto/LoginReqDTO.java 0 → 100644
  1 +package com.example.erp.module.usr.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import lombok.Getter;
  5 +import lombok.Setter;
  6 +
  7 +@Getter
  8 +@Setter
  9 +public class LoginReqDTO {
  10 +
  11 + @NotBlank(message = "公司编号不能为空")
  12 + private String brandNo;
  13 +
  14 + @NotBlank(message = "用户名不能为空")
  15 + private String username;
  16 +
  17 + @NotBlank(message = "密码不能为空")
  18 + private String password;
  19 +}
... ...
backend/src/main/java/com/example/erp/module/usr/dto/RefreshTokenReqDTO.java 0 → 100644
  1 +package com.example.erp.module.usr.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import lombok.Getter;
  5 +import lombok.Setter;
  6 +
  7 +@Getter
  8 +@Setter
  9 +public class RefreshTokenReqDTO {
  10 +
  11 + @NotBlank(message = "refreshToken 不能为空")
  12 + private String refreshToken;
  13 +}
... ...
backend/src/main/java/com/example/erp/module/usr/dto/UserCreateReqDTO.java 0 → 100644
  1 +package com.example.erp.module.usr.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import jakarta.validation.constraints.Pattern;
  5 +import lombok.Getter;
  6 +import lombok.Setter;
  7 +
  8 +import java.util.List;
  9 +
  10 +@Getter
  11 +@Setter
  12 +public class UserCreateReqDTO {
  13 +
  14 + @NotBlank(message = "用户号不能为空")
  15 + private String userCode;
  16 +
  17 + @NotBlank(message = "用户名不能为空")
  18 + private String username;
  19 +
  20 + @NotBlank(message = "用户类型不能为空")
  21 + @Pattern(regexp = "普通用户|超级管理员", message = "用户类型无效")
  22 + private String userType;
  23 +
  24 + @NotBlank(message = "语言不能为空")
  25 + @Pattern(regexp = "中文|英文|繁体", message = "语言无效")
  26 + private String language;
  27 +
  28 + private boolean canEditDoc = false;
  29 +
  30 + private String employeeId;
  31 +
  32 + private List<String> permGroupIds;
  33 +}
... ...
backend/src/main/java/com/example/erp/module/usr/dto/UserListQueryDTO.java 0 → 100644
  1 +package com.example.erp.module.usr.dto;
  2 +
  3 +import lombok.Getter;
  4 +import lombok.Setter;
  5 +
  6 +@Getter
  7 +@Setter
  8 +public class UserListQueryDTO {
  9 +
  10 + private String queryField = "username";
  11 + private String matchType = "contains";
  12 + private String queryValue;
  13 + private int page = 1;
  14 + private int pageSize = 20;
  15 +}
... ...
backend/src/main/java/com/example/erp/module/usr/dto/UserUpdateReqDTO.java 0 → 100644
  1 +package com.example.erp.module.usr.dto;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import jakarta.validation.constraints.Pattern;
  6 +import lombok.Getter;
  7 +import lombok.Setter;
  8 +
  9 +import java.util.List;
  10 +
  11 +@Getter
  12 +@Setter
  13 +public class UserUpdateReqDTO {
  14 +
  15 + @NotBlank(message = "用户类型不能为空")
  16 + @Pattern(regexp = "普通用户|超级管理员", message = "用户类型无效")
  17 + private String userType;
  18 +
  19 + @NotBlank(message = "语言不能为空")
  20 + @Pattern(regexp = "中文|英文|繁体", message = "语言无效")
  21 + private String language;
  22 + private boolean canEditDoc;
  23 + private boolean isDisabled;
  24 + private String employeeId;
  25 + @NotNull(message = "权限组列表不能为空")
  26 + private List<String> permGroupIds;
  27 +}
... ...
backend/src/main/java/com/example/erp/module/usr/entity/BrandEntity.java 0 → 100644
  1 +package com.example.erp.module.usr.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableField;
  5 +import com.baomidou.mybatisplus.annotation.TableId;
  6 +import com.baomidou.mybatisplus.annotation.TableName;
  7 +import lombok.Getter;
  8 +import lombok.Setter;
  9 +
  10 +import java.time.LocalDateTime;
  11 +
  12 +@Getter
  13 +@Setter
  14 +@TableName("brand")
  15 +public class BrandEntity {
  16 +
  17 + @TableId(value = "iIncrement", type = IdType.AUTO)
  18 + private Integer iIncrement;
  19 +
  20 + @TableField("sId")
  21 + private String sId;
  22 +
  23 + @TableField("sBrandsId")
  24 + private String sBrandsId;
  25 +
  26 + @TableField("sSubsidiaryId")
  27 + private String sSubsidiaryId;
  28 +
  29 + @TableField("tCreateDate")
  30 + private LocalDateTime tCreateDate;
  31 +
  32 + @TableField("sName")
  33 + private String sName;
  34 +
  35 + @TableField("sShortName")
  36 + private String sShortName;
  37 +
  38 + @TableField("sNo")
  39 + private String sNo;
  40 +}
... ...
backend/src/main/java/com/example/erp/module/usr/entity/PermissionGroupEntity.java 0 → 100644
  1 +package com.example.erp.module.usr.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableField;
  5 +import com.baomidou.mybatisplus.annotation.TableId;
  6 +import com.baomidou.mybatisplus.annotation.TableName;
  7 +import lombok.Getter;
  8 +import lombok.Setter;
  9 +
  10 +import java.time.LocalDateTime;
  11 +
  12 +@Getter
  13 +@Setter
  14 +@TableName("usr_permission_group")
  15 +public class PermissionGroupEntity {
  16 +
  17 + @TableId(value = "iIncrement", type = IdType.AUTO)
  18 + private Integer iIncrement;
  19 +
  20 + @TableField("sId")
  21 + private String sId;
  22 +
  23 + @TableField("sBrandsId")
  24 + private String sBrandsId;
  25 +
  26 + @TableField("sSubsidiaryId")
  27 + private String sSubsidiaryId;
  28 +
  29 + @TableField("tCreateDate")
  30 + private LocalDateTime tCreateDate;
  31 +
  32 + @TableField("sGroupCode")
  33 + private String sGroupCode;
  34 +
  35 + @TableField("sGroupName")
  36 + private String sGroupName;
  37 +
  38 + @TableField("sCategory")
  39 + private String sCategory;
  40 +}
... ...
backend/src/main/java/com/example/erp/module/usr/entity/StaffEntity.java 0 → 100644
  1 +package com.example.erp.module.usr.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableField;
  5 +import com.baomidou.mybatisplus.annotation.TableId;
  6 +import com.baomidou.mybatisplus.annotation.TableName;
  7 +import lombok.Getter;
  8 +import lombok.Setter;
  9 +
  10 +import java.time.LocalDateTime;
  11 +
  12 +@Getter
  13 +@Setter
  14 +@TableName("tStaff")
  15 +public class StaffEntity {
  16 +
  17 + @TableId(value = "iIncrement", type = IdType.AUTO)
  18 + private Integer iIncrement;
  19 +
  20 + @TableField("sId")
  21 + private String sId;
  22 +
  23 + @TableField("sBrandsId")
  24 + private String sBrandsId;
  25 +
  26 + @TableField("sSubsidiaryId")
  27 + private String sSubsidiaryId;
  28 +
  29 + @TableField("tCreateDate")
  30 + private LocalDateTime tCreateDate;
  31 +
  32 + @TableField("sStaffNo")
  33 + private String sStaffNo;
  34 +
  35 + @TableField("sStaffName")
  36 + private String sStaffName;
  37 +
  38 + @TableField("sDepartment")
  39 + private String sDepartment;
  40 +
  41 + @TableField("sCreatedBy")
  42 + private String sCreatedBy;
  43 +
  44 + @TableField("bDeleted")
  45 + private Integer bDeleted;
  46 +
  47 + @TableField("tDeletedDate")
  48 + private LocalDateTime tDeletedDate;
  49 +
  50 + @TableField("sDeletedBy")
  51 + private String sDeletedBy;
  52 +}
... ...
backend/src/main/java/com/example/erp/module/usr/entity/UserPermissionEntity.java 0 → 100644
  1 +package com.example.erp.module.usr.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableField;
  5 +import com.baomidou.mybatisplus.annotation.TableId;
  6 +import com.baomidou.mybatisplus.annotation.TableName;
  7 +import lombok.Getter;
  8 +import lombok.Setter;
  9 +
  10 +import java.time.LocalDateTime;
  11 +
  12 +@Getter
  13 +@Setter
  14 +@TableName("usr_user_permission")
  15 +public class UserPermissionEntity {
  16 +
  17 + @TableId(value = "iIncrement", type = IdType.AUTO)
  18 + private Integer iIncrement;
  19 +
  20 + @TableField("sId")
  21 + private String sId;
  22 +
  23 + @TableField("sBrandsId")
  24 + private String sBrandsId;
  25 +
  26 + @TableField("sSubsidiaryId")
  27 + private String sSubsidiaryId;
  28 +
  29 + @TableField("tCreateDate")
  30 + private LocalDateTime tCreateDate;
  31 +
  32 + @TableField("sUserId")
  33 + private String sUserId;
  34 +
  35 + @TableField("sPermGroupId")
  36 + private String sPermGroupId;
  37 +}
... ...
backend/src/main/java/com/example/erp/module/usr/entity/UsrUserEntity.java 0 → 100644
  1 +package com.example.erp.module.usr.entity;
  2 +
  3 +import com.baomidou.mybatisplus.annotation.IdType;
  4 +import com.baomidou.mybatisplus.annotation.TableField;
  5 +import com.baomidou.mybatisplus.annotation.TableId;
  6 +import com.baomidou.mybatisplus.annotation.TableName;
  7 +import lombok.Getter;
  8 +import lombok.Setter;
  9 +
  10 +import java.time.LocalDateTime;
  11 +
  12 +@Getter
  13 +@Setter
  14 +@TableName("usr_user")
  15 +public class UsrUserEntity {
  16 +
  17 + @TableId(value = "iIncrement", type = IdType.AUTO)
  18 + private Integer iIncrement;
  19 +
  20 + @TableField("sId")
  21 + private String sId;
  22 +
  23 + @TableField("sBrandsId")
  24 + private String sBrandsId;
  25 +
  26 + @TableField("sSubsidiaryId")
  27 + private String sSubsidiaryId;
  28 +
  29 + @TableField("tCreateDate")
  30 + private LocalDateTime tCreateDate;
  31 +
  32 + @TableField("sUserCode")
  33 + private String sUserCode;
  34 +
  35 + @TableField("sUsername")
  36 + private String sUsername;
  37 +
  38 + @TableField("sPasswordHash")
  39 + private String sPasswordHash;
  40 +
  41 + @TableField("sUserType")
  42 + private String sUserType;
  43 +
  44 + @TableField("sLanguage")
  45 + private String sLanguage;
  46 +
  47 + @TableField("bCanEditDoc")
  48 + private Integer bCanEditDoc;
  49 +
  50 + @TableField("bIsDisabled")
  51 + private Integer bIsDisabled;
  52 +
  53 + @TableField("sEmployeeId")
  54 + private String sEmployeeId;
  55 +
  56 + @TableField("sCreatorUsername")
  57 + private String sCreatorUsername;
  58 +
  59 + @TableField("tLastLoginDate")
  60 + private LocalDateTime tLastLoginDate;
  61 +
  62 + @TableField("iLoginFailCount")
  63 + private Integer iLoginFailCount;
  64 +
  65 + @TableField("tLockUntil")
  66 + private LocalDateTime tLockUntil;
  67 +}
... ...
backend/src/main/java/com/example/erp/module/usr/mapper/BrandMapper.java 0 → 100644
  1 +package com.example.erp.module.usr.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.example.erp.module.usr.entity.BrandEntity;
  5 +
  6 +public interface BrandMapper extends BaseMapper<BrandEntity> {
  7 +}
... ...
backend/src/main/java/com/example/erp/module/usr/mapper/PermissionGroupMapper.java 0 → 100644
  1 +package com.example.erp.module.usr.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.example.erp.module.usr.entity.PermissionGroupEntity;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface PermissionGroupMapper extends BaseMapper<PermissionGroupEntity> {}
... ...
backend/src/main/java/com/example/erp/module/usr/mapper/StaffMapper.java 0 → 100644
  1 +package com.example.erp.module.usr.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.example.erp.module.usr.entity.StaffEntity;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface StaffMapper extends BaseMapper<StaffEntity> {}
... ...
backend/src/main/java/com/example/erp/module/usr/mapper/UserPermissionMapper.java 0 → 100644
  1 +package com.example.erp.module.usr.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  4 +import com.example.erp.module.usr.entity.UserPermissionEntity;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface UserPermissionMapper extends BaseMapper<UserPermissionEntity> {}
... ...
backend/src/main/java/com/example/erp/module/usr/mapper/UsrUserMapper.java 0 → 100644
  1 +package com.example.erp.module.usr.mapper;
  2 +
  3 +import com.baomidou.mybatisplus.core.metadata.IPage;
  4 +import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  5 +import com.example.erp.module.usr.entity.UsrUserEntity;
  6 +import com.example.erp.module.usr.vo.UserListItemVO;
  7 +import org.apache.ibatis.annotations.Param;
  8 +
  9 +public interface UsrUserMapper extends BaseMapper<UsrUserEntity> {
  10 +
  11 + IPage<UserListItemVO> selectUserList(
  12 + IPage<UserListItemVO> page,
  13 + @Param("brandId") String brandId,
  14 + @Param("queryField") String queryField,
  15 + @Param("matchType") String matchType,
  16 + @Param("queryValue") String queryValue
  17 + );
  18 +}
... ...
backend/src/main/java/com/example/erp/module/usr/service/AuthService.java 0 → 100644
  1 +package com.example.erp.module.usr.service;
  2 +
  3 +import com.example.erp.module.usr.dto.LoginReqDTO;
  4 +import com.example.erp.module.usr.vo.BrandVO;
  5 +import com.example.erp.module.usr.vo.LoginVO;
  6 +
  7 +import java.util.List;
  8 +
  9 +public interface AuthService {
  10 +
  11 + LoginVO login(LoginReqDTO req);
  12 +
  13 + String refresh(String refreshToken);
  14 +
  15 + List<BrandVO> getBrands();
  16 +}
... ...
backend/src/main/java/com/example/erp/module/usr/service/UserService.java 0 → 100644
  1 +package com.example.erp.module.usr.service;
  2 +
  3 +import com.example.erp.common.vo.PageVO;
  4 +import com.example.erp.config.UserPrincipal;
  5 +import com.example.erp.module.usr.dto.UserCreateReqDTO;
  6 +import com.example.erp.module.usr.dto.UserListQueryDTO;
  7 +import com.example.erp.module.usr.dto.UserUpdateReqDTO;
  8 +import com.example.erp.module.usr.vo.PermissionGroupVO;
  9 +import com.example.erp.module.usr.vo.StaffVO;
  10 +import com.example.erp.module.usr.vo.UserCreateRespVO;
  11 +import com.example.erp.module.usr.vo.UserListItemVO;
  12 +import com.example.erp.module.usr.vo.UserUpdateRespVO;
  13 +
  14 +import java.util.List;
  15 +
  16 +public interface UserService {
  17 +
  18 + UserCreateRespVO createUser(UserCreateReqDTO req, UserPrincipal principal);
  19 +
  20 + List<StaffVO> getStaffs(String brandId);
  21 +
  22 + List<PermissionGroupVO> getPermissionGroups(String brandId);
  23 +
  24 + PageVO<UserListItemVO> getUserList(UserListQueryDTO query, String brandId);
  25 +
  26 + UserUpdateRespVO updateUser(String userId, UserUpdateReqDTO req, UserPrincipal principal);
  27 +}
... ...
backend/src/main/java/com/example/erp/module/usr/service/impl/AuthServiceImpl.java 0 → 100644
  1 +package com.example.erp.module.usr.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  5 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  6 +import com.example.erp.common.constants.AuthErrorCode;
  7 +import com.example.erp.common.exception.BizException;
  8 +import com.example.erp.common.util.JwtUtil;
  9 +import com.example.erp.module.usr.dto.LoginReqDTO;
  10 +import com.example.erp.module.usr.entity.BrandEntity;
  11 +import com.example.erp.module.usr.entity.UsrUserEntity;
  12 +import com.example.erp.module.usr.mapper.BrandMapper;
  13 +import com.example.erp.module.usr.mapper.UsrUserMapper;
  14 +import com.example.erp.module.usr.service.AuthService;
  15 +import com.example.erp.module.usr.vo.BrandVO;
  16 +import com.example.erp.module.usr.vo.LoginVO;
  17 +import io.jsonwebtoken.Claims;
  18 +import lombok.RequiredArgsConstructor;
  19 +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  20 +import org.springframework.stereotype.Service;
  21 +import org.springframework.transaction.annotation.Transactional;
  22 +
  23 +import java.time.LocalDateTime;
  24 +import java.time.temporal.ChronoUnit;
  25 +import java.util.List;
  26 +import java.util.stream.Collectors;
  27 +
  28 +@Service
  29 +@RequiredArgsConstructor
  30 +public class AuthServiceImpl implements AuthService {
  31 +
  32 + private final BrandMapper brandMapper;
  33 + private final UsrUserMapper userMapper;
  34 + private final JwtUtil jwtUtil;
  35 + private final BCryptPasswordEncoder passwordEncoder;
  36 +
  37 + @Override
  38 + @Transactional
  39 + public LoginVO login(LoginReqDTO req) {
  40 + // 1. 查 brand
  41 + BrandEntity brand = brandMapper.selectOne(
  42 + new LambdaQueryWrapper<BrandEntity>().eq(BrandEntity::getSNo, req.getBrandNo()));
  43 + if (brand == null) {
  44 + throw new BizException(AuthErrorCode.USERNAME_OR_PASSWORD_ERROR, "用户名或密码错误");
  45 + }
  46 +
  47 + // 2. 查 user(多租户)
  48 + UsrUserEntity user = userMapper.selectOne(
  49 + new LambdaQueryWrapper<UsrUserEntity>()
  50 + .eq(UsrUserEntity::getSUsername, req.getUsername())
  51 + .eq(UsrUserEntity::getSBrandsId, brand.getSId()));
  52 + if (user == null) {
  53 + throw new BizException(AuthErrorCode.USERNAME_OR_PASSWORD_ERROR, "用户名或密码错误");
  54 + }
  55 +
  56 + // 3. 禁用检查
  57 + if (Integer.valueOf(1).equals(user.getBIsDisabled())) {
  58 + throw new BizException(AuthErrorCode.ACCOUNT_DISABLED, "账号已被禁用,请联系管理员");
  59 + }
  60 +
  61 + // 4. 锁定检查
  62 + if (user.getTLockUntil() != null && user.getTLockUntil().isAfter(LocalDateTime.now())) {
  63 + long seconds = ChronoUnit.SECONDS.between(LocalDateTime.now(), user.getTLockUntil());
  64 + int minutes = (int) Math.ceil(seconds / 60.0);
  65 + throw new BizException(AuthErrorCode.ACCOUNT_LOCKED, "账号已被锁定,请 " + minutes + " 分钟后重试");
  66 + }
  67 +
  68 + // 5. 密码校验
  69 + if (!passwordEncoder.matches(req.getPassword(), user.getSPasswordHash())) {
  70 + int newCount = (user.getILoginFailCount() == null ? 0 : user.getILoginFailCount()) + 1;
  71 + LambdaUpdateWrapper<UsrUserEntity> updateWrapper = new LambdaUpdateWrapper<UsrUserEntity>()
  72 + .eq(UsrUserEntity::getSId, user.getSId())
  73 + .set(UsrUserEntity::getILoginFailCount, newCount);
  74 + if (newCount >= 5) {
  75 + LocalDateTime lockUntil = LocalDateTime.now().plusMinutes(30);
  76 + updateWrapper.set(UsrUserEntity::getTLockUntil, lockUntil);
  77 + userMapper.update(null, updateWrapper);
  78 + throw new BizException(AuthErrorCode.ACCOUNT_LOCKED, "账号已被锁定,请 30 分钟后重试");
  79 + }
  80 + userMapper.update(null, updateWrapper);
  81 + throw new BizException(AuthErrorCode.USERNAME_OR_PASSWORD_ERROR, "用户名或密码错误");
  82 + }
  83 +
  84 + // 6. 登录成功
  85 + userMapper.update(null, new LambdaUpdateWrapper<UsrUserEntity>()
  86 + .eq(UsrUserEntity::getSId, user.getSId())
  87 + .set(UsrUserEntity::getILoginFailCount, 0)
  88 + .set(UsrUserEntity::getTLockUntil, null)
  89 + .set(UsrUserEntity::getTLastLoginDate, LocalDateTime.now()));
  90 +
  91 + String accessToken = jwtUtil.generateAccessToken(
  92 + user.getSId(), user.getSUsername(), user.getSUserType(), brand.getSId());
  93 + String refreshToken = jwtUtil.generateRefreshToken(user.getSId(), brand.getSId());
  94 +
  95 + LoginVO.UserInfoVO userInfo = new LoginVO.UserInfoVO();
  96 + userInfo.setUserId(user.getSId());
  97 + userInfo.setUsername(user.getSUsername());
  98 + userInfo.setUserType(user.getSUserType());
  99 + userInfo.setLanguage(user.getSLanguage());
  100 + userInfo.setBrandId(brand.getSId());
  101 +
  102 + LoginVO vo = new LoginVO();
  103 + vo.setAccessToken(accessToken);
  104 + vo.setRefreshToken(refreshToken);
  105 + vo.setExpiresIn(86400L);
  106 + vo.setUserInfo(userInfo);
  107 + return vo;
  108 + }
  109 +
  110 + @Override
  111 + public String refresh(String refreshToken) {
  112 + Claims claims = jwtUtil.parseRefreshToken(refreshToken);
  113 + String userId = claims.getSubject();
  114 + String brandId = claims.get("brandId", String.class);
  115 +
  116 + UsrUserEntity user = userMapper.selectOne(
  117 + new LambdaQueryWrapper<UsrUserEntity>().eq(UsrUserEntity::getSId, userId));
  118 + if (user == null
  119 + || Integer.valueOf(1).equals(user.getBIsDisabled())
  120 + || (user.getTLockUntil() != null && user.getTLockUntil().isAfter(LocalDateTime.now()))) {
  121 + throw new BizException(AuthErrorCode.REFRESH_TOKEN_INVALID, "Refresh Token 已失效,请重新登录");
  122 + }
  123 +
  124 + return jwtUtil.generateAccessToken(user.getSId(), user.getSUsername(), user.getSUserType(), brandId);
  125 + }
  126 +
  127 + @Override
  128 + public List<BrandVO> getBrands() {
  129 + List<BrandEntity> brands = brandMapper.selectList(
  130 + new QueryWrapper<BrandEntity>().select("sNo", "sName").orderByAsc("sName"));
  131 + return brands.stream().map(b -> {
  132 + BrandVO vo = new BrandVO();
  133 + vo.setSNo(b.getSNo());
  134 + vo.setSName(b.getSName());
  135 + return vo;
  136 + }).collect(Collectors.toList());
  137 + }
  138 +}
... ...
backend/src/main/java/com/example/erp/module/usr/service/impl/UserServiceImpl.java 0 → 100644
  1 +package com.example.erp.module.usr.service.impl;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  5 +import com.baomidou.mybatisplus.core.metadata.IPage;
  6 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  7 +import com.example.erp.common.constants.UsrErrorCode;
  8 +import com.example.erp.common.exception.BizException;
  9 +import com.example.erp.common.vo.PageVO;
  10 +import com.example.erp.config.UserPrincipal;
  11 +import com.example.erp.module.usr.dto.UserCreateReqDTO;
  12 +import com.example.erp.module.usr.dto.UserListQueryDTO;
  13 +import com.example.erp.module.usr.dto.UserUpdateReqDTO;
  14 +import com.example.erp.module.usr.entity.PermissionGroupEntity;
  15 +import com.example.erp.module.usr.entity.StaffEntity;
  16 +import com.example.erp.module.usr.entity.UserPermissionEntity;
  17 +import com.example.erp.module.usr.entity.UsrUserEntity;
  18 +import com.example.erp.module.usr.mapper.PermissionGroupMapper;
  19 +import com.example.erp.module.usr.mapper.StaffMapper;
  20 +import com.example.erp.module.usr.mapper.UserPermissionMapper;
  21 +import com.example.erp.module.usr.mapper.UsrUserMapper;
  22 +import com.example.erp.module.usr.service.UserService;
  23 +import com.example.erp.module.usr.vo.PermissionGroupVO;
  24 +import com.example.erp.module.usr.vo.StaffVO;
  25 +import com.example.erp.module.usr.vo.UserCreateRespVO;
  26 +import com.example.erp.module.usr.vo.UserListItemVO;
  27 +import com.example.erp.module.usr.vo.UserUpdateRespVO;
  28 +import lombok.RequiredArgsConstructor;
  29 +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  30 +import org.springframework.stereotype.Service;
  31 +import org.springframework.transaction.annotation.Transactional;
  32 +
  33 +import java.time.LocalDateTime;
  34 +import java.util.List;
  35 +import java.util.UUID;
  36 +import java.util.stream.Collectors;
  37 +
  38 +@Service
  39 +@RequiredArgsConstructor
  40 +public class UserServiceImpl implements UserService {
  41 +
  42 + private final UsrUserMapper userMapper;
  43 + private final StaffMapper staffMapper;
  44 + private final PermissionGroupMapper permGroupMapper;
  45 + private final UserPermissionMapper userPermissionMapper;
  46 + private final BCryptPasswordEncoder passwordEncoder;
  47 +
  48 + @Override
  49 + @Transactional
  50 + public UserCreateRespVO createUser(UserCreateReqDTO req, UserPrincipal principal) {
  51 + if (!"超级管理员".equals(principal.userType())) {
  52 + throw new BizException(UsrErrorCode.PERMISSION_DENIED, "权限不足");
  53 + }
  54 +
  55 + if (userMapper.selectCount(new LambdaQueryWrapper<UsrUserEntity>()
  56 + .eq(UsrUserEntity::getSUserCode, req.getUserCode())) > 0) {
  57 + throw new BizException(UsrErrorCode.USER_CODE_EXISTS, "用户号已存在");
  58 + }
  59 +
  60 + if (userMapper.selectCount(new LambdaQueryWrapper<UsrUserEntity>()
  61 + .eq(UsrUserEntity::getSUsername, req.getUsername())
  62 + .eq(UsrUserEntity::getSBrandsId, principal.brandId())) > 0) {
  63 + throw new BizException(UsrErrorCode.USERNAME_EXISTS, "用户名已存在");
  64 + }
  65 +
  66 + if (req.getEmployeeId() != null) {
  67 + StaffEntity staff = staffMapper.selectOne(new LambdaQueryWrapper<StaffEntity>()
  68 + .eq(StaffEntity::getSId, req.getEmployeeId())
  69 + .eq(StaffEntity::getSBrandsId, principal.brandId()));
  70 + if (staff == null) {
  71 + throw new BizException(UsrErrorCode.EMPLOYEE_NOT_FOUND, "员工不存在");
  72 + }
  73 + }
  74 +
  75 + UsrUserEntity user = new UsrUserEntity();
  76 + user.setSId(UUID.randomUUID().toString());
  77 + user.setSBrandsId(principal.brandId());
  78 + user.setSCreatorUsername(principal.username());
  79 + user.setTCreateDate(LocalDateTime.now());
  80 + user.setSUserCode(req.getUserCode());
  81 + user.setSUsername(req.getUsername());
  82 + user.setSPasswordHash(passwordEncoder.encode("666666"));
  83 + user.setSUserType(req.getUserType());
  84 + user.setSLanguage(req.getLanguage());
  85 + user.setBCanEditDoc(req.isCanEditDoc() ? 1 : 0);
  86 + user.setBIsDisabled(0);
  87 + user.setSEmployeeId(req.getEmployeeId());
  88 + user.setILoginFailCount(0);
  89 + userMapper.insert(user);
  90 +
  91 + if (req.getPermGroupIds() != null && !req.getPermGroupIds().isEmpty()) {
  92 + List<UserPermissionEntity> perms = req.getPermGroupIds().stream()
  93 + .map(groupId -> {
  94 + UserPermissionEntity perm = new UserPermissionEntity();
  95 + perm.setSId(UUID.randomUUID().toString());
  96 + perm.setSBrandsId(principal.brandId());
  97 + perm.setTCreateDate(LocalDateTime.now());
  98 + perm.setSUserId(user.getSId());
  99 + perm.setSPermGroupId(groupId);
  100 + return perm;
  101 + })
  102 + .collect(Collectors.toList());
  103 + userPermissionMapper.insert(perms);
  104 + }
  105 +
  106 + UserCreateRespVO vo = new UserCreateRespVO();
  107 + vo.setUserId(user.getSId());
  108 + vo.setUserCode(user.getSUserCode());
  109 + vo.setUsername(user.getSUsername());
  110 + return vo;
  111 + }
  112 +
  113 + @Override
  114 + @Transactional(readOnly = true)
  115 + public List<StaffVO> getStaffs(String brandId) {
  116 + List<StaffEntity> staffs = staffMapper.selectList(new LambdaQueryWrapper<StaffEntity>()
  117 + .eq(StaffEntity::getSBrandsId, brandId)
  118 + .eq(StaffEntity::getBDeleted, 0));
  119 + return staffs.stream().map(s -> {
  120 + StaffVO vo = new StaffVO();
  121 + vo.setSId(s.getSId());
  122 + vo.setSStaffName(s.getSStaffName());
  123 + return vo;
  124 + }).collect(Collectors.toList());
  125 + }
  126 +
  127 + // REQ-USR-003: 查询用户
  128 + @Override
  129 + @Transactional(readOnly = true)
  130 + public PageVO<UserListItemVO> getUserList(UserListQueryDTO query, String brandId) {
  131 + int cappedSize = Math.min(query.getPageSize(), 100);
  132 + IPage<UserListItemVO> iPage = new Page<>(query.getPage(), cappedSize);
  133 + String queryValue = query.getQueryValue() == null ? "" : query.getQueryValue();
  134 + IPage<UserListItemVO> result = userMapper.selectUserList(iPage, brandId, query.getQueryField(), query.getMatchType(), queryValue);
  135 + return PageVO.of(result);
  136 + }
  137 +
  138 + @Override
  139 + @Transactional(readOnly = true)
  140 + public List<PermissionGroupVO> getPermissionGroups(String brandId) {
  141 + List<PermissionGroupEntity> groups = permGroupMapper.selectList(new LambdaQueryWrapper<PermissionGroupEntity>()
  142 + .eq(PermissionGroupEntity::getSBrandsId, brandId));
  143 + return groups.stream().map(g -> {
  144 + PermissionGroupVO vo = new PermissionGroupVO();
  145 + vo.setSId(g.getSId());
  146 + vo.setSGroupCode(g.getSGroupCode());
  147 + vo.setSGroupName(g.getSGroupName());
  148 + vo.setSCategory(g.getSCategory());
  149 + return vo;
  150 + }).collect(Collectors.toList());
  151 + }
  152 +
  153 + // REQ-USR-002: 修改用户
  154 + @Override
  155 + @Transactional
  156 + public UserUpdateRespVO updateUser(String userId, UserUpdateReqDTO req, UserPrincipal principal) {
  157 + if (!"超级管理员".equals(principal.userType())) {
  158 + throw new BizException(UsrErrorCode.PERMISSION_DENIED, "权限不足");
  159 + }
  160 +
  161 + UsrUserEntity existing = userMapper.selectOne(new LambdaQueryWrapper<UsrUserEntity>()
  162 + .eq(UsrUserEntity::getSId, userId)
  163 + .eq(UsrUserEntity::getSBrandsId, principal.brandId()));
  164 + if (existing == null) {
  165 + throw new BizException(UsrErrorCode.USER_NOT_FOUND, "用户不存在");
  166 + }
  167 +
  168 + if (principal.userId().equals(userId) && !"超级管理员".equals(req.getUserType())) {
  169 + throw new BizException(UsrErrorCode.SELF_ADMIN_CHANGE, "禁止修改自己的管理员角色");
  170 + }
  171 +
  172 + if (req.getEmployeeId() != null) {
  173 + StaffEntity staff = staffMapper.selectOne(new LambdaQueryWrapper<StaffEntity>()
  174 + .eq(StaffEntity::getSId, req.getEmployeeId())
  175 + .eq(StaffEntity::getSBrandsId, principal.brandId()));
  176 + if (staff == null) {
  177 + throw new BizException(UsrErrorCode.EMPLOYEE_NOT_FOUND, "员工不存在");
  178 + }
  179 + }
  180 +
  181 + userMapper.update(null, new LambdaUpdateWrapper<UsrUserEntity>()
  182 + .eq(UsrUserEntity::getSId, userId)
  183 + .eq(UsrUserEntity::getSBrandsId, principal.brandId())
  184 + .set(UsrUserEntity::getSUserType, req.getUserType())
  185 + .set(UsrUserEntity::getSLanguage, req.getLanguage())
  186 + .set(UsrUserEntity::getBCanEditDoc, req.isCanEditDoc() ? 1 : 0)
  187 + .set(UsrUserEntity::getBIsDisabled, req.isDisabled() ? 1 : 0)
  188 + .set(UsrUserEntity::getSEmployeeId, req.getEmployeeId()));
  189 +
  190 + userPermissionMapper.delete(new LambdaQueryWrapper<UserPermissionEntity>()
  191 + .eq(UserPermissionEntity::getSUserId, userId)
  192 + .eq(UserPermissionEntity::getSBrandsId, principal.brandId()));
  193 +
  194 + if (req.getPermGroupIds() != null && !req.getPermGroupIds().isEmpty()) {
  195 + List<UserPermissionEntity> perms = req.getPermGroupIds().stream()
  196 + .map(groupId -> {
  197 + UserPermissionEntity perm = new UserPermissionEntity();
  198 + perm.setSId(UUID.randomUUID().toString());
  199 + perm.setSBrandsId(principal.brandId());
  200 + perm.setTCreateDate(LocalDateTime.now());
  201 + perm.setSUserId(userId);
  202 + perm.setSPermGroupId(groupId);
  203 + return perm;
  204 + })
  205 + .collect(Collectors.toList());
  206 + userPermissionMapper.insert(perms);
  207 + }
  208 +
  209 + UserUpdateRespVO vo = new UserUpdateRespVO();
  210 + vo.setUserId(userId);
  211 + vo.setUsername(existing.getSUsername());
  212 + vo.setUpdatedAt(LocalDateTime.now());
  213 + return vo;
  214 + }
  215 +}
... ...
backend/src/main/java/com/example/erp/module/usr/vo/BrandVO.java 0 → 100644
  1 +package com.example.erp.module.usr.vo;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +import lombok.Getter;
  5 +import lombok.Setter;
  6 +
  7 +@Getter
  8 +@Setter
  9 +public class BrandVO {
  10 + @JsonProperty("sNo")
  11 + private String sNo;
  12 +
  13 + @JsonProperty("sName")
  14 + private String sName;
  15 +}
... ...
backend/src/main/java/com/example/erp/module/usr/vo/LoginVO.java 0 → 100644
  1 +package com.example.erp.module.usr.vo;
  2 +
  3 +import lombok.Getter;
  4 +import lombok.Setter;
  5 +
  6 +@Getter
  7 +@Setter
  8 +public class LoginVO {
  9 +
  10 + private String accessToken;
  11 + private String refreshToken;
  12 + private long expiresIn;
  13 + private UserInfoVO userInfo;
  14 +
  15 + @Getter
  16 + @Setter
  17 + public static class UserInfoVO {
  18 + private String userId;
  19 + private String username;
  20 + private String userType;
  21 + private String language;
  22 + private String brandId;
  23 + }
  24 +}
... ...
backend/src/main/java/com/example/erp/module/usr/vo/PermissionGroupVO.java 0 → 100644
  1 +package com.example.erp.module.usr.vo;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +import lombok.Getter;
  5 +import lombok.Setter;
  6 +
  7 +@Getter
  8 +@Setter
  9 +public class PermissionGroupVO {
  10 + @JsonProperty("sId")
  11 + private String sId;
  12 + @JsonProperty("sGroupCode")
  13 + private String sGroupCode;
  14 + @JsonProperty("sGroupName")
  15 + private String sGroupName;
  16 + @JsonProperty("sCategory")
  17 + private String sCategory;
  18 +}
... ...
backend/src/main/java/com/example/erp/module/usr/vo/StaffVO.java 0 → 100644
  1 +package com.example.erp.module.usr.vo;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +import lombok.Getter;
  5 +import lombok.Setter;
  6 +
  7 +@Getter
  8 +@Setter
  9 +public class StaffVO {
  10 + @JsonProperty("sId")
  11 + private String sId;
  12 + @JsonProperty("sStaffName")
  13 + private String sStaffName;
  14 +}
... ...
backend/src/main/java/com/example/erp/module/usr/vo/UserCreateRespVO.java 0 → 100644
  1 +package com.example.erp.module.usr.vo;
  2 +
  3 +import lombok.Getter;
  4 +import lombok.Setter;
  5 +
  6 +@Getter
  7 +@Setter
  8 +public class UserCreateRespVO {
  9 + private String userId;
  10 + private String userCode;
  11 + private String username;
  12 +}
... ...
backend/src/main/java/com/example/erp/module/usr/vo/UserListItemVO.java 0 → 100644
  1 +package com.example.erp.module.usr.vo;
  2 +
  3 +import com.fasterxml.jackson.annotation.JsonProperty;
  4 +import lombok.Getter;
  5 +import lombok.Setter;
  6 +
  7 +import java.time.LocalDateTime;
  8 +
  9 +@Getter
  10 +@Setter
  11 +public class UserListItemVO {
  12 +
  13 + @JsonProperty("sId")
  14 + private String sId;
  15 +
  16 + @JsonProperty("sUsername")
  17 + private String sUsername;
  18 +
  19 + @JsonProperty("sUserCode")
  20 + private String sUserCode;
  21 +
  22 + @JsonProperty("sUserType")
  23 + private String sUserType;
  24 +
  25 + @JsonProperty("sLanguage")
  26 + private String sLanguage;
  27 +
  28 + @JsonProperty("bCanEditDoc")
  29 + private Integer bCanEditDoc;
  30 +
  31 + @JsonProperty("bIsDisabled")
  32 + private Integer bIsDisabled;
  33 +
  34 + @JsonProperty("tLastLoginDate")
  35 + private LocalDateTime tLastLoginDate;
  36 +
  37 + @JsonProperty("sCreatorUsername")
  38 + private String sCreatorUsername;
  39 +
  40 + @JsonProperty("tCreateDate")
  41 + private LocalDateTime tCreateDate;
  42 +
  43 + @JsonProperty("sStaffName")
  44 + private String sStaffName;
  45 +
  46 + @JsonProperty("sDepartment")
  47 + private String sDepartment;
  48 +}
... ...
backend/src/main/java/com/example/erp/module/usr/vo/UserUpdateRespVO.java 0 → 100644
  1 +package com.example.erp.module.usr.vo;
  2 +
  3 +import lombok.Getter;
  4 +import lombok.Setter;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Getter
  9 +@Setter
  10 +public class UserUpdateRespVO {
  11 +
  12 + private String userId;
  13 + private String username;
  14 + private LocalDateTime updatedAt;
  15 +}
... ...
backend/src/main/resources/application-dev.yml 0 → 100644
  1 +logging:
  2 + level:
  3 + com.example.erp: DEBUG
  4 + org.springframework.security: DEBUG
... ...
backend/src/main/resources/application.yml 0 → 100644
  1 +spring:
  2 + datasource:
  3 + url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_SCHEMA}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=UTF-8
  4 + username: ${DB_USER}
  5 + password: ${DB_PASSWORD}
  6 + driver-class-name: com.mysql.cj.jdbc.Driver
  7 + flyway:
  8 + locations: classpath:db/migration
  9 + baseline-on-migrate: true
  10 + baseline-version: 1
  11 + validate-on-migrate: false
  12 + out-of-order: false
  13 +
  14 +server:
  15 + port: 8080
  16 +
  17 +jwt:
  18 + secret: ${JWT_SECRET}
  19 + access-token-expiry: 86400
  20 + refresh-token-expiry: 604800
  21 +
  22 +mybatis-plus:
  23 + configuration:
  24 + map-underscore-to-camel-case: false
  25 + global-config:
  26 + db-config:
  27 + id-type: auto
... ...
backend/src/main/resources/db/migration/V1__initial_schema.sql 0 → 100644
  1 +-- Flyway migration V1 — initial schema for 小羚羊
  2 +-- Generated: 2026-05-08T01:01:55Z
  3 +-- Source: 由 A4 db-init 从 docs/03-数据库设计文档.md 翻译生成(schema SSoT 是 docs/03)
  4 +-- This is the FIRST migration; subsequent schema changes must be written as new files sql/migrations/V2__<desc>.sql, V3__... etc.
  5 +-- Apply: Flyway runs this automatically at Spring Boot startup.
  6 +-- Do not hand-edit this file after it is committed; write a new migration instead.
  7 +
  8 +SET NAMES utf8mb4;
  9 +SET CHARACTER_SET_CLIENT = utf8mb4;
  10 +
  11 +-- ============================================================
  12 +-- Table: usr_user
  13 +-- ============================================================
  14 +CREATE TABLE `usr_user` (
  15 + `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  16 + `sId` VARCHAR(100) NULL COMMENT '业务 ID(标准列)',
  17 + `sBrandsId` VARCHAR(100) NULL COMMENT '品牌 ID(多租户隔离,标准列)',
  18 + `sSubsidiaryId` VARCHAR(100) NULL COMMENT '子公司 ID(组织层级隔离,标准列)',
  19 + `tCreateDate` DATETIME NOT NULL COMMENT '创建时间(标准列)',
  20 + `sUserCode` VARCHAR(50) NOT NULL COMMENT '用户号(业务编号,人类可读唯一标识)',
  21 + `sUsername` VARCHAR(100) NOT NULL COMMENT '用户名(登录标识,全局唯一,不可修改)',
  22 + `sPasswordHash` VARCHAR(255) NOT NULL COMMENT 'BCrypt 哈希密码,禁止存储明文',
  23 + `sUserType` VARCHAR(20) NOT NULL DEFAULT '普通用户' COMMENT '用户类型:普通用户 / 超级管理员',
  24 + `sLanguage` VARCHAR(20) NOT NULL DEFAULT '中文' COMMENT '界面语言:中文 / 英文 / 繁体',
  25 + `bCanEditDoc` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '单据修改权限:0=否,1=是',
  26 + `bIsDisabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否作废/禁用:0=正常,1=禁用',
  27 + `sEmployeeId` VARCHAR(100) NULL COMMENT '关联职员 ID(跨模块引用,职员未关联时为 NULL)',
  28 + `sCreatorUsername` VARCHAR(100) NULL COMMENT '制单人用户名(冗余字段,便于列表展示)',
  29 + `tLastLoginDate` DATETIME NULL COMMENT '最后登录时间',
  30 + `iLoginFailCount` INT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数,用于防暴力破解',
  31 + `tLockUntil` DATETIME NULL COMMENT '账号锁定截止时间,NULL 表示未锁定',
  32 + PRIMARY KEY (`iIncrement`)
  33 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  34 + COMMENT='用户账户主表,存储登录信息、类型、语言偏好及安全控制字段';
  35 +
  36 +-- ============================================================
  37 +-- Table: usr_permission_group
  38 +-- ============================================================
  39 +CREATE TABLE `usr_permission_group` (
  40 + `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  41 + `sId` VARCHAR(100) NULL COMMENT '业务 ID(标准列)',
  42 + `sBrandsId` VARCHAR(100) NULL COMMENT '品牌 ID(多租户隔离,标准列)',
  43 + `sSubsidiaryId` VARCHAR(100) NULL COMMENT '子公司 ID(组织层级隔离,标准列)',
  44 + `tCreateDate` DATETIME NOT NULL COMMENT '创建时间(标准列)',
  45 + `sGroupCode` VARCHAR(100) NOT NULL COMMENT '权限代码(如 usr:create、usr:edit),全局唯一',
  46 + `sGroupName` VARCHAR(200) NOT NULL COMMENT '权限显示名称(如"新增用户"、"修改用户")',
  47 + `sCategory` VARCHAR(100) NULL COMMENT '权限分类标签,用于前端权限分组展示',
  48 + PRIMARY KEY (`iIncrement`)
  49 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  50 + COMMENT='权限分类/权限组定义表,每行对应一个可分配给用户的权限项';
  51 +
  52 +-- ============================================================
  53 +-- Table: usr_user_permission
  54 +-- ============================================================
  55 +CREATE TABLE `usr_user_permission` (
  56 + `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  57 + `sId` VARCHAR(100) NULL COMMENT '业务 ID(标准列)',
  58 + `sBrandsId` VARCHAR(100) NULL COMMENT '品牌 ID(多租户隔离,标准列)',
  59 + `sSubsidiaryId` VARCHAR(100) NULL COMMENT '子公司 ID(组织层级隔离,标准列)',
  60 + `tCreateDate` DATETIME NOT NULL COMMENT '创建时间(标准列)',
  61 + `sUserId` VARCHAR(100) NOT NULL COMMENT '关联 usr_user.sId',
  62 + `sPermGroupId` VARCHAR(100) NOT NULL COMMENT '关联 usr_permission_group.sId',
  63 + PRIMARY KEY (`iIncrement`)
  64 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  65 + COMMENT='用户与权限组的多对多关联表';
  66 +
  67 +-- ============================================================
  68 +-- Table: tStaff
  69 +-- ============================================================
  70 +CREATE TABLE `tStaff` (
  71 + `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  72 + `sId` VARCHAR(100) NULL COMMENT '业务 ID(标准列)',
  73 + `sBrandsId` VARCHAR(100) NULL COMMENT '品牌 ID(多租户隔离,标准列)',
  74 + `sSubsidiaryId` VARCHAR(100) NULL COMMENT '子公司 ID(组织层级隔离,标准列)',
  75 + `tCreateDate` DATETIME NOT NULL COMMENT '创建时间(标准列)',
  76 + `sStaffNo` VARCHAR(50) NULL COMMENT '职员编号;系统内唯一',
  77 + `sStaffName` VARCHAR(50) NOT NULL COMMENT '职员姓名',
  78 + `sDepartment` VARCHAR(100) NULL DEFAULT NULL COMMENT '所属部门(本期暂用字符串,未来如需独立 tDepartment 字典表再另行重构)',
  79 + `sCreatedBy` VARCHAR(50) NULL COMMENT '制单人',
  80 + `bDeleted` BIT(1) NOT NULL DEFAULT 0 COMMENT '软删除标记',
  81 + `tDeletedDate` DATETIME NULL DEFAULT NULL COMMENT '软删除时间',
  82 + `sDeletedBy` VARCHAR(50) NULL DEFAULT NULL COMMENT '软删除操作人',
  83 + PRIMARY KEY (`iIncrement`)
  84 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  85 + COMMENT='职员维度(员工名 / 部门 / 编号)';
  86 +
  87 +-- ============================================================
  88 +-- Table: brand
  89 +-- ============================================================
  90 +CREATE TABLE `brand` (
  91 + `iIncrement` INT NOT NULL AUTO_INCREMENT COMMENT '整数主键 ID(标准列)',
  92 + `sId` VARCHAR(100) NULL COMMENT '业务 ID(标准列)',
  93 + `sBrandsId` VARCHAR(100) NULL COMMENT '品牌 ID(多租户隔离,标准列)',
  94 + `sSubsidiaryId` VARCHAR(100) NULL COMMENT '子公司 ID(组织层级隔离,标准列)',
  95 + `tCreateDate` DATETIME NOT NULL COMMENT '创建时间(标准列)',
  96 + `sName` VARCHAR(100) NULL COMMENT '公司名称',
  97 + `sShortName` VARCHAR(100) NULL COMMENT '公司简称',
  98 + `sNo` VARCHAR(100) NULL COMMENT '单位编号(登录账号根据单位编号作为前缀)',
  99 + PRIMARY KEY (`iIncrement`)
  100 +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
  101 + COMMENT='公司表';
  102 +
  103 +-- ============================================================
  104 +-- Indexes: usr_user
  105 +-- ============================================================
  106 +CREATE UNIQUE INDEX `uk_usr_user_sid` ON `usr_user` (`sId`);
  107 +CREATE UNIQUE INDEX `uk_usr_user_username` ON `usr_user` (`sUsername`);
  108 +CREATE UNIQUE INDEX `uk_usr_user_usercode` ON `usr_user` (`sUserCode`);
  109 +CREATE INDEX `idx_usr_user_tenant` ON `usr_user` (`sBrandsId`, `sSubsidiaryId`);
  110 +CREATE INDEX `idx_usr_user_type` ON `usr_user` (`sUserType`);
  111 +CREATE INDEX `idx_usr_user_disabled` ON `usr_user` (`bIsDisabled`);
  112 +
  113 +-- ============================================================
  114 +-- Indexes: usr_permission_group
  115 +-- ============================================================
  116 +CREATE UNIQUE INDEX `uk_usr_perm_group_sid` ON `usr_permission_group` (`sId`);
  117 +CREATE UNIQUE INDEX `uk_usr_perm_group_code` ON `usr_permission_group` (`sGroupCode`);
  118 +CREATE INDEX `idx_usr_perm_group_tenant` ON `usr_permission_group` (`sBrandsId`, `sSubsidiaryId`);
  119 +
  120 +-- ============================================================
  121 +-- Indexes: usr_user_permission
  122 +-- ============================================================
  123 +CREATE UNIQUE INDEX `uk_usr_user_perm` ON `usr_user_permission` (`sUserId`, `sPermGroupId`);
  124 +CREATE INDEX `idx_usr_user_perm_user` ON `usr_user_permission` (`sUserId`);
  125 +CREATE INDEX `idx_usr_user_perm_group` ON `usr_user_permission` (`sPermGroupId`);
  126 +
  127 +-- ============================================================
  128 +-- Indexes: tStaff
  129 +-- ============================================================
  130 +CREATE UNIQUE INDEX `uk_staff_no` ON `tStaff` (`sStaffNo`);
  131 +CREATE INDEX `idx_staff_name` ON `tStaff` (`sStaffName`);
  132 +CREATE INDEX `idx_department` ON `tStaff` (`sDepartment`);
  133 +
  134 +-- ============================================================
  135 +-- Indexes: brand
  136 +-- ============================================================
  137 +CREATE UNIQUE INDEX `uk_brand_no` ON `brand` (`sNo`);
  138 +CREATE INDEX `idx_brand_name` ON `brand` (`sName`);
  139 +
  140 +-- ============================================================
  141 +-- Foreign Keys
  142 +-- ============================================================
  143 +ALTER TABLE `usr_user_permission`
  144 + ADD CONSTRAINT `fk_usr_user_perm_user`
  145 + FOREIGN KEY (`sUserId`) REFERENCES `usr_user` (`sId`)
  146 + ON DELETE CASCADE ON UPDATE CASCADE;
  147 +
  148 +ALTER TABLE `usr_user_permission`
  149 + ADD CONSTRAINT `fk_usr_user_perm_group`
  150 + FOREIGN KEY (`sPermGroupId`) REFERENCES `usr_permission_group` (`sId`)
  151 + ON DELETE CASCADE ON UPDATE CASCADE;
... ...
backend/src/main/resources/db/migration/V2__fix_username_unique_per_tenant.sql 0 → 100644
  1 +-- Flyway migration V2 — fix usr_user username uniqueness to per-tenant scope
  2 +-- Generated: 2026-05-08
  3 +-- Reason: uk_usr_user_username was globally unique; same username must be allowed across different brands (sBrandsId)
  4 +-- New unique constraint: (sUsername, sBrandsId) composite
  5 +
  6 +ALTER TABLE usr_user DROP INDEX uk_usr_user_username;
  7 +CREATE UNIQUE INDEX uk_usr_user_username_tenant ON usr_user (sUsername, sBrandsId);
... ...
backend/src/main/resources/mapper/UsrUserMapper.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  3 + "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  4 +<mapper namespace="com.example.erp.module.usr.mapper.UsrUserMapper">
  5 +
  6 + <!-- REQ-USR-003: 查询用户 -->
  7 + <select id="selectUserList" resultType="com.example.erp.module.usr.vo.UserListItemVO">
  8 + SELECT u.sId, u.sUsername, u.sUserCode, u.sUserType, u.sLanguage,
  9 + u.bCanEditDoc, u.bIsDisabled, u.tLastLoginDate, u.sCreatorUsername, u.tCreateDate,
  10 + s.sStaffName, s.sDepartment
  11 + FROM usr_user u
  12 + LEFT JOIN tStaff s ON u.sEmployeeId = s.sId
  13 + AND s.sBrandsId = u.sBrandsId
  14 + AND s.bDeleted = 0
  15 + WHERE u.sBrandsId = #{brandId}
  16 + <if test="queryValue != null and queryValue != ''">
  17 + <choose>
  18 + <when test="queryField == 'username'">
  19 + <choose>
  20 + <when test="matchType == 'contains'">AND u.sUsername LIKE CONCAT('%', #{queryValue}, '%')</when>
  21 + <when test="matchType == 'notContains'">AND u.sUsername NOT LIKE CONCAT('%', #{queryValue}, '%')</when>
  22 + <otherwise>AND u.sUsername = #{queryValue}</otherwise>
  23 + </choose>
  24 + </when>
  25 + <when test="queryField == 'staffName'">
  26 + <choose>
  27 + <when test="matchType == 'contains'">AND s.sStaffName LIKE CONCAT('%', #{queryValue}, '%')</when>
  28 + <when test="matchType == 'notContains'">AND (s.sStaffName IS NULL OR s.sStaffName NOT LIKE CONCAT('%', #{queryValue}, '%'))</when>
  29 + <otherwise>AND s.sStaffName = #{queryValue}</otherwise>
  30 + </choose>
  31 + </when>
  32 + <when test="queryField == 'userCode'">
  33 + <choose>
  34 + <when test="matchType == 'contains'">AND u.sUserCode LIKE CONCAT('%', #{queryValue}, '%')</when>
  35 + <when test="matchType == 'notContains'">AND u.sUserCode NOT LIKE CONCAT('%', #{queryValue}, '%')</when>
  36 + <otherwise>AND u.sUserCode = #{queryValue}</otherwise>
  37 + </choose>
  38 + </when>
  39 + <when test="queryField == 'department'">
  40 + <choose>
  41 + <when test="matchType == 'contains'">AND s.sDepartment LIKE CONCAT('%', #{queryValue}, '%')</when>
  42 + <when test="matchType == 'notContains'">AND (s.sDepartment IS NULL OR s.sDepartment NOT LIKE CONCAT('%', #{queryValue}, '%'))</when>
  43 + <otherwise>AND s.sDepartment = #{queryValue}</otherwise>
  44 + </choose>
  45 + </when>
  46 + <when test="queryField == 'userType'">
  47 + <choose>
  48 + <when test="matchType == 'contains'">AND u.sUserType LIKE CONCAT('%', #{queryValue}, '%')</when>
  49 + <when test="matchType == 'notContains'">AND u.sUserType NOT LIKE CONCAT('%', #{queryValue}, '%')</when>
  50 + <otherwise>AND u.sUserType = #{queryValue}</otherwise>
  51 + </choose>
  52 + </when>
  53 + <when test="queryField == 'creator'">
  54 + <choose>
  55 + <when test="matchType == 'contains'">AND u.sCreatorUsername LIKE CONCAT('%', #{queryValue}, '%')</when>
  56 + <when test="matchType == 'notContains'">AND u.sCreatorUsername NOT LIKE CONCAT('%', #{queryValue}, '%')</when>
  57 + <otherwise>AND u.sCreatorUsername = #{queryValue}</otherwise>
  58 + </choose>
  59 + </when>
  60 + <when test="queryField == 'disabled'">
  61 + <choose>
  62 + <when test="queryValue == '是'">AND u.bIsDisabled = 1</when>
  63 + <when test="queryValue == '否'">AND u.bIsDisabled = 0</when>
  64 + </choose>
  65 + </when>
  66 + <when test="queryField == 'lastLoginDate'">
  67 + AND DATE(u.tLastLoginDate) = #{queryValue}
  68 + </when>
  69 + </choose>
  70 + </if>
  71 + ORDER BY u.tCreateDate DESC
  72 + </select>
  73 +
  74 +</mapper>
... ...
backend/src/test/java/com/example/erp/ApplicationContextTest.java 0 → 100644
  1 +package com.example.erp;
  2 +
  3 +import org.junit.jupiter.api.Test;
  4 +import org.springframework.boot.test.context.SpringBootTest;
  5 +import org.springframework.test.context.ActiveProfiles;
  6 +
  7 +@SpringBootTest
  8 +@ActiveProfiles("test")
  9 +class ApplicationContextTest {
  10 +
  11 + @Test
  12 + void contextLoads() {
  13 + }
  14 +}
... ...
backend/src/test/java/com/example/erp/common/JwtUtilTest.java 0 → 100644
  1 +package com.example.erp.common;
  2 +
  3 +import com.example.erp.common.exception.BizException;
  4 +import com.example.erp.common.util.JwtUtil;
  5 +import com.example.erp.config.JwtProperties;
  6 +import io.jsonwebtoken.Claims;
  7 +import org.junit.jupiter.api.BeforeEach;
  8 +import org.junit.jupiter.api.Test;
  9 +
  10 +import static org.junit.jupiter.api.Assertions.*;
  11 +
  12 +class JwtUtilTest {
  13 +
  14 + private JwtUtil jwtUtil;
  15 +
  16 + @BeforeEach
  17 + void setUp() {
  18 + JwtProperties props = new JwtProperties();
  19 + props.setSecret("testSecretKey32CharactersMinimumXXXXX");
  20 + props.setAccessTokenExpiry(86400L);
  21 + props.setRefreshTokenExpiry(604800L);
  22 + jwtUtil = new JwtUtil(props);
  23 + }
  24 +
  25 + @Test
  26 + void generateAndParseAccessToken_containsAllClaims() {
  27 + String token = jwtUtil.generateAccessToken("u1", "admin", "超级管理员", "b1");
  28 + Claims claims = jwtUtil.parseAccessToken(token);
  29 +
  30 + assertEquals("u1", claims.getSubject());
  31 + assertEquals("admin", claims.get("username", String.class));
  32 + assertEquals("超级管理员", claims.get("userType", String.class));
  33 + assertEquals("b1", claims.get("brandId", String.class));
  34 + assertNotNull(claims.getExpiration());
  35 + }
  36 +
  37 + @Test
  38 + void parseRefreshToken_withAccessToken_throws40103() {
  39 + String accessToken = jwtUtil.generateAccessToken("u1", "admin", "普通用户", "b1");
  40 + BizException ex = assertThrows(BizException.class, () -> jwtUtil.parseRefreshToken(accessToken));
  41 + assertEquals(40103, ex.getCode());
  42 + }
  43 +
  44 + @Test
  45 + void parseAccessToken_withExpiredToken_throws40103() {
  46 + JwtProperties expiredProps = new JwtProperties();
  47 + expiredProps.setSecret("testSecretKey32CharactersMinimumXXXXX");
  48 + expiredProps.setAccessTokenExpiry(-1L);
  49 + expiredProps.setRefreshTokenExpiry(604800L);
  50 + JwtUtil expiredJwtUtil = new JwtUtil(expiredProps);
  51 +
  52 + String token = expiredJwtUtil.generateAccessToken("u1", "admin", "普通用户", "b1");
  53 + BizException ex = assertThrows(BizException.class, () -> jwtUtil.parseAccessToken(token));
  54 + assertEquals(40103, ex.getCode());
  55 + }
  56 +}
... ...
backend/src/test/java/com/example/erp/common/ResultTest.java 0 → 100644
  1 +package com.example.erp.common;
  2 +
  3 +import com.example.erp.common.response.Result;
  4 +import org.junit.jupiter.api.Test;
  5 +
  6 +import static org.junit.jupiter.api.Assertions.*;
  7 +
  8 +class ResultTest {
  9 +
  10 + @Test
  11 + void ok_setsCode200AndData() {
  12 + Result<String> result = Result.ok("hello");
  13 + assertEquals(200, result.getCode());
  14 + assertEquals("hello", result.getData());
  15 + assertEquals("操作成功", result.getMessage());
  16 + }
  17 +
  18 + @Test
  19 + void fail_setsCodeAndNullData() {
  20 + Result<Object> result = Result.fail(40100, "用户名或密码错误");
  21 + assertEquals(40100, result.getCode());
  22 + assertNull(result.getData());
  23 + assertEquals("用户名或密码错误", result.getMessage());
  24 + }
  25 +
  26 + @Test
  27 + void ok_hasPositiveTimestamp() {
  28 + Result<Void> result = Result.ok(null);
  29 + assertTrue(result.getTimestamp() > 0);
  30 + }
  31 +}
... ...
backend/src/test/java/com/example/erp/module/usr/AuthControllerTest.java 0 → 100644
  1 +package com.example.erp.module.usr;
  2 +
  3 +import com.example.erp.common.exception.BizException;
  4 +import com.example.erp.module.usr.dto.LoginReqDTO;
  5 +import com.example.erp.module.usr.service.AuthService;
  6 +import com.example.erp.module.usr.vo.BrandVO;
  7 +import com.example.erp.module.usr.vo.LoginVO;
  8 +import com.fasterxml.jackson.databind.ObjectMapper;
  9 +import org.junit.jupiter.api.Test;
  10 +import org.springframework.beans.factory.annotation.Autowired;
  11 +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
  12 +import org.springframework.boot.test.mock.mockito.MockBean;
  13 +import org.springframework.context.annotation.Import;
  14 +import org.springframework.http.MediaType;
  15 +import org.springframework.test.web.servlet.MockMvc;
  16 +
  17 +import java.util.List;
  18 +import java.util.Map;
  19 +
  20 +import static org.mockito.ArgumentMatchers.any;
  21 +import static org.mockito.ArgumentMatchers.anyString;
  22 +import static org.mockito.Mockito.when;
  23 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
  24 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
  25 +
  26 +import com.example.erp.config.SecurityConfig;
  27 +import com.example.erp.config.JwtAuthenticationFilter;
  28 +import com.example.erp.config.BeanConfig;
  29 +import com.example.erp.common.util.JwtUtil;
  30 +import com.example.erp.config.JwtProperties;
  31 +import com.example.erp.module.usr.controller.AuthController;
  32 +
  33 +@WebMvcTest(controllers = AuthController.class)
  34 +@Import({SecurityConfig.class, JwtAuthenticationFilter.class, BeanConfig.class, JwtUtil.class, JwtProperties.class})
  35 +class AuthControllerTest {
  36 +
  37 + @Autowired private MockMvc mockMvc;
  38 + @Autowired private ObjectMapper objectMapper;
  39 + @MockBean private AuthService authService;
  40 +
  41 + @Test
  42 + void login_wrongPassword_returns40100() throws Exception {
  43 + when(authService.login(any())).thenThrow(new BizException(40100, "用户名或密码错误"));
  44 +
  45 + Map<String, String> body = Map.of("brandNo", "STD", "username", "admin", "password", "wrong");
  46 + mockMvc.perform(post("/api/auth/login")
  47 + .contentType(MediaType.APPLICATION_JSON)
  48 + .content(objectMapper.writeValueAsString(body)))
  49 + .andExpect(status().isOk())
  50 + .andExpect(jsonPath("$.code").value(40100))
  51 + .andExpect(jsonPath("$.message").value("用户名或密码错误"));
  52 + }
  53 +
  54 + @Test
  55 + void login_validCredentials_returns200AndTokens() throws Exception {
  56 + LoginVO.UserInfoVO userInfo = new LoginVO.UserInfoVO();
  57 + userInfo.setUserId("u1");
  58 + userInfo.setUsername("admin");
  59 + userInfo.setUserType("普通用户");
  60 + userInfo.setLanguage("中文");
  61 + userInfo.setBrandId("b1");
  62 +
  63 + LoginVO loginVO = new LoginVO();
  64 + loginVO.setAccessToken("access-token");
  65 + loginVO.setRefreshToken("refresh-token");
  66 + loginVO.setExpiresIn(86400L);
  67 + loginVO.setUserInfo(userInfo);
  68 +
  69 + when(authService.login(any())).thenReturn(loginVO);
  70 +
  71 + Map<String, String> body = Map.of("brandNo", "STD", "username", "admin", "password", "666666");
  72 + mockMvc.perform(post("/api/auth/login")
  73 + .contentType(MediaType.APPLICATION_JSON)
  74 + .content(objectMapper.writeValueAsString(body)))
  75 + .andExpect(status().isOk())
  76 + .andExpect(jsonPath("$.code").value(200))
  77 + .andExpect(jsonPath("$.data.accessToken").value("access-token"))
  78 + .andExpect(jsonPath("$.data.userInfo.userId").value("u1"));
  79 + }
  80 +
  81 + @Test
  82 + void refresh_withInvalidToken_returns40103() throws Exception {
  83 + when(authService.refresh(anyString())).thenThrow(new BizException(40103, "Refresh Token 已失效,请重新登录"));
  84 +
  85 + Map<String, String> body = Map.of("refreshToken", "invalid-token");
  86 + mockMvc.perform(post("/api/auth/refresh")
  87 + .contentType(MediaType.APPLICATION_JSON)
  88 + .content(objectMapper.writeValueAsString(body)))
  89 + .andExpect(status().isOk())
  90 + .andExpect(jsonPath("$.code").value(40103));
  91 + }
  92 +
  93 + @Test
  94 + void getBrands_returns200AndList() throws Exception {
  95 + BrandVO b = new BrandVO();
  96 + b.setSNo("STD");
  97 + b.setSName("标准版");
  98 + when(authService.getBrands()).thenReturn(List.of(b));
  99 +
  100 + mockMvc.perform(get("/api/auth/brands"))
  101 + .andExpect(status().isOk())
  102 + .andExpect(jsonPath("$.code").value(200))
  103 + .andExpect(jsonPath("$.data[0].sNo").value("STD"))
  104 + .andExpect(jsonPath("$.data[0].sName").value("标准版"));
  105 + }
  106 +}
... ...
backend/src/test/java/com/example/erp/module/usr/AuthServiceTest.java 0 → 100644
  1 +package com.example.erp.module.usr;
  2 +
  3 +import com.example.erp.common.exception.BizException;
  4 +import com.example.erp.common.util.JwtUtil;
  5 +import com.example.erp.module.usr.dto.LoginReqDTO;
  6 +import com.example.erp.module.usr.entity.BrandEntity;
  7 +import com.example.erp.module.usr.entity.UsrUserEntity;
  8 +import com.example.erp.module.usr.mapper.BrandMapper;
  9 +import com.example.erp.module.usr.mapper.UsrUserMapper;
  10 +import com.example.erp.module.usr.service.impl.AuthServiceImpl;
  11 +import com.example.erp.module.usr.vo.BrandVO;
  12 +import com.example.erp.module.usr.vo.LoginVO;
  13 +import io.jsonwebtoken.Claims;
  14 +import com.baomidou.mybatisplus.core.MybatisConfiguration;
  15 +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
  16 +import org.apache.ibatis.builder.MapperBuilderAssistant;
  17 +import org.junit.jupiter.api.BeforeAll;
  18 +import org.junit.jupiter.api.BeforeEach;
  19 +import org.junit.jupiter.api.Test;
  20 +import org.junit.jupiter.api.extension.ExtendWith;
  21 +import org.mockito.InjectMocks;
  22 +import org.mockito.Mock;
  23 +import org.mockito.junit.jupiter.MockitoExtension;
  24 +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  25 +
  26 +import java.time.LocalDateTime;
  27 +import java.util.List;
  28 +import java.util.Map;
  29 +
  30 +import static org.junit.jupiter.api.Assertions.*;
  31 +import static org.mockito.ArgumentMatchers.*;
  32 +import static org.mockito.Mockito.*;
  33 +
  34 +@ExtendWith(MockitoExtension.class)
  35 +class AuthServiceTest {
  36 +
  37 + @BeforeAll
  38 + static void initMyBatisEntityCache() {
  39 + TableInfoHelper.initTableInfo(
  40 + new MapperBuilderAssistant(new MybatisConfiguration(), ""),
  41 + UsrUserEntity.class);
  42 + }
  43 +
  44 +
  45 + @Mock private BrandMapper brandMapper;
  46 + @Mock private UsrUserMapper userMapper;
  47 + @Mock private JwtUtil jwtUtil;
  48 + @Mock private BCryptPasswordEncoder passwordEncoder;
  49 +
  50 + @InjectMocks
  51 + private AuthServiceImpl authService;
  52 +
  53 + private LoginReqDTO req;
  54 + private BrandEntity brand;
  55 + private UsrUserEntity user;
  56 +
  57 + @BeforeEach
  58 + void setUp() {
  59 + req = new LoginReqDTO();
  60 + req.setBrandNo("STD");
  61 + req.setUsername("admin");
  62 + req.setPassword("666666");
  63 +
  64 + brand = new BrandEntity();
  65 + brand.setSId("b1");
  66 + brand.setSNo("STD");
  67 + brand.setSName("标准版");
  68 +
  69 + user = new UsrUserEntity();
  70 + user.setSId("u1");
  71 + user.setSUsername("admin");
  72 + user.setSPasswordHash("$2a$10$hashed");
  73 + user.setSUserType("普通用户");
  74 + user.setSLanguage("中文");
  75 + user.setBIsDisabled(0);
  76 + user.setILoginFailCount(0);
  77 + user.setTLockUntil(null);
  78 + }
  79 +
  80 + @Test
  81 + void login_brandNotFound_throws40100() {
  82 + when(brandMapper.selectOne(any())).thenReturn(null);
  83 + BizException ex = assertThrows(BizException.class, () -> authService.login(req));
  84 + assertEquals(40100, ex.getCode());
  85 + }
  86 +
  87 + @Test
  88 + void login_userNotFound_throws40100() {
  89 + when(brandMapper.selectOne(any())).thenReturn(brand);
  90 + when(userMapper.selectOne(any())).thenReturn(null);
  91 + BizException ex = assertThrows(BizException.class, () -> authService.login(req));
  92 + assertEquals(40100, ex.getCode());
  93 + }
  94 +
  95 + @Test
  96 + void login_accountDisabled_throws40101() {
  97 + user.setBIsDisabled(1);
  98 + when(brandMapper.selectOne(any())).thenReturn(brand);
  99 + when(userMapper.selectOne(any())).thenReturn(user);
  100 + BizException ex = assertThrows(BizException.class, () -> authService.login(req));
  101 + assertEquals(40101, ex.getCode());
  102 + }
  103 +
  104 + @Test
  105 + void login_accountLocked_throws40102WithRemainingMinutes() {
  106 + user.setTLockUntil(LocalDateTime.now().plusMinutes(20));
  107 + when(brandMapper.selectOne(any())).thenReturn(brand);
  108 + when(userMapper.selectOne(any())).thenReturn(user);
  109 + BizException ex = assertThrows(BizException.class, () -> authService.login(req));
  110 + assertEquals(40102, ex.getCode());
  111 + assertTrue(ex.getMessage().contains("分钟"));
  112 + }
  113 +
  114 + @Test
  115 + void login_wrongPassword_firstTime_throws40100AndIncrementsCount() {
  116 + when(brandMapper.selectOne(any())).thenReturn(brand);
  117 + when(userMapper.selectOne(any())).thenReturn(user);
  118 + when(passwordEncoder.matches(anyString(), anyString())).thenReturn(false);
  119 + BizException ex = assertThrows(BizException.class, () -> authService.login(req));
  120 + assertEquals(40100, ex.getCode());
  121 + verify(userMapper).update(isNull(), any());
  122 + }
  123 +
  124 + @Test
  125 + void login_wrongPassword_5thTime_setsLockAndThrows40102() {
  126 + user.setILoginFailCount(4);
  127 + when(brandMapper.selectOne(any())).thenReturn(brand);
  128 + when(userMapper.selectOne(any())).thenReturn(user);
  129 + when(passwordEncoder.matches(anyString(), anyString())).thenReturn(false);
  130 + BizException ex = assertThrows(BizException.class, () -> authService.login(req));
  131 + assertEquals(40102, ex.getCode());
  132 + verify(userMapper).update(isNull(), any());
  133 + }
  134 +
  135 + @Test
  136 + void login_success_resetsCountAndReturnsTokens() {
  137 + when(brandMapper.selectOne(any())).thenReturn(brand);
  138 + when(userMapper.selectOne(any())).thenReturn(user);
  139 + when(passwordEncoder.matches(anyString(), anyString())).thenReturn(true);
  140 + when(jwtUtil.generateAccessToken(anyString(), anyString(), anyString(), anyString())).thenReturn("access-token");
  141 + when(jwtUtil.generateRefreshToken(anyString(), anyString())).thenReturn("refresh-token");
  142 +
  143 + LoginVO result = authService.login(req);
  144 +
  145 + assertEquals("access-token", result.getAccessToken());
  146 + assertEquals("refresh-token", result.getRefreshToken());
  147 + assertEquals(86400L, result.getExpiresIn());
  148 + assertNotNull(result.getUserInfo());
  149 + assertEquals("u1", result.getUserInfo().getUserId());
  150 + verify(userMapper).update(isNull(), any());
  151 + }
  152 +
  153 + // ---- Task 6: refresh + getBrands tests ----
  154 +
  155 + @Test
  156 + void refresh_validRefreshToken_returnsNewAccessToken() {
  157 + Claims claims = mock(Claims.class);
  158 + when(claims.getSubject()).thenReturn("u1");
  159 + when(claims.get("brandId", String.class)).thenReturn("b1");
  160 + when(jwtUtil.parseRefreshToken("valid-refresh")).thenReturn(claims);
  161 + when(userMapper.selectOne(any())).thenReturn(user);
  162 + when(jwtUtil.generateAccessToken(anyString(), anyString(), anyString(), anyString())).thenReturn("new-access-token");
  163 +
  164 + String newToken = authService.refresh("valid-refresh");
  165 + assertEquals("new-access-token", newToken);
  166 + }
  167 +
  168 + @Test
  169 + void refresh_lockedUser_throws40103() {
  170 + Claims claims = mock(Claims.class);
  171 + when(claims.getSubject()).thenReturn("u1");
  172 + when(claims.get("brandId", String.class)).thenReturn("b1");
  173 + when(jwtUtil.parseRefreshToken("valid-refresh")).thenReturn(claims);
  174 + user.setTLockUntil(LocalDateTime.now().plusMinutes(25));
  175 + when(userMapper.selectOne(any())).thenReturn(user);
  176 +
  177 + BizException ex = assertThrows(BizException.class, () -> authService.refresh("valid-refresh"));
  178 + assertEquals(40103, ex.getCode());
  179 + }
  180 +
  181 + @Test
  182 + void refresh_invalidRefreshToken_throws40103() {
  183 + when(jwtUtil.parseRefreshToken("bad-token"))
  184 + .thenThrow(new BizException(40103, "Refresh Token 已失效,请重新登录"));
  185 + BizException ex = assertThrows(BizException.class, () -> authService.refresh("bad-token"));
  186 + assertEquals(40103, ex.getCode());
  187 + }
  188 +
  189 + @Test
  190 + void getBrands_returnsListSortedByName() {
  191 + BrandEntity b1 = new BrandEntity();
  192 + b1.setSNo("STD"); b1.setSName("标准版");
  193 + BrandEntity b2 = new BrandEntity();
  194 + b2.setSNo("ENT"); b2.setSName("企业版");
  195 + when(brandMapper.selectList(any())).thenReturn(List.of(b1, b2));
  196 +
  197 + List<BrandVO> result = authService.getBrands();
  198 + assertEquals(2, result.size());
  199 + assertEquals("标准版", result.get(0).getSName());
  200 + }
  201 +}
... ...
backend/src/test/java/com/example/erp/module/usr/BrandMapperTest.java 0 → 100644
  1 +package com.example.erp.module.usr;
  2 +
  3 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  4 +import com.example.erp.module.usr.entity.BrandEntity;
  5 +import com.example.erp.module.usr.mapper.BrandMapper;
  6 +import org.junit.jupiter.api.AfterEach;
  7 +import org.junit.jupiter.api.BeforeEach;
  8 +import org.junit.jupiter.api.Test;
  9 +import org.springframework.beans.factory.annotation.Autowired;
  10 +import org.springframework.boot.test.context.SpringBootTest;
  11 +import org.springframework.test.context.ActiveProfiles;
  12 +
  13 +import java.time.LocalDateTime;
  14 +
  15 +import static org.junit.jupiter.api.Assertions.*;
  16 +
  17 +@SpringBootTest
  18 +@ActiveProfiles("test")
  19 +class BrandMapperTest {
  20 +
  21 + @Autowired
  22 + private BrandMapper brandMapper;
  23 +
  24 + @BeforeEach
  25 + void setUp() {
  26 + BrandEntity brand = new BrandEntity();
  27 + brand.setSId("b-test-mapper-001");
  28 + brand.setSNo("TST");
  29 + brand.setSName("测试版");
  30 + brand.setTCreateDate(LocalDateTime.now());
  31 + brandMapper.insert(brand);
  32 + }
  33 +
  34 + @AfterEach
  35 + void tearDown() {
  36 + brandMapper.delete(new LambdaQueryWrapper<BrandEntity>().eq(BrandEntity::getSNo, "TST"));
  37 + }
  38 +
  39 + @Test
  40 + void findByNo_returnsCorrectBrand() {
  41 + BrandEntity found = brandMapper.selectOne(
  42 + new LambdaQueryWrapper<BrandEntity>().eq(BrandEntity::getSNo, "TST"));
  43 + assertNotNull(found);
  44 + assertEquals("测试版", found.getSName());
  45 + assertEquals("b-test-mapper-001", found.getSId());
  46 + }
  47 +}
... ...
backend/src/test/java/com/example/erp/module/usr/UserControllerTest.java 0 → 100644
  1 +package com.example.erp.module.usr;
  2 +
  3 +import com.example.erp.config.BeanConfig;
  4 +import com.example.erp.config.JwtAuthenticationFilter;
  5 +import com.example.erp.config.JwtProperties;
  6 +import com.example.erp.config.SecurityConfig;
  7 +import com.example.erp.common.util.JwtUtil;
  8 +import com.example.erp.module.usr.controller.UserController;
  9 +import com.example.erp.module.usr.service.UserService;
  10 +import com.example.erp.common.vo.PageVO;
  11 +import com.example.erp.module.usr.dto.UserListQueryDTO;
  12 +import com.example.erp.module.usr.dto.UserUpdateReqDTO;
  13 +import com.example.erp.module.usr.vo.UserCreateRespVO;
  14 +import com.example.erp.module.usr.vo.UserListItemVO;
  15 +import com.example.erp.module.usr.vo.UserUpdateRespVO;
  16 +import com.example.erp.module.usr.vo.StaffVO;
  17 +import com.example.erp.module.usr.vo.PermissionGroupVO;
  18 +import com.fasterxml.jackson.databind.ObjectMapper;
  19 +import org.junit.jupiter.api.Test;
  20 +import org.springframework.beans.factory.annotation.Autowired;
  21 +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
  22 +import org.springframework.boot.test.mock.mockito.MockBean;
  23 +import org.springframework.context.annotation.Import;
  24 +import org.springframework.http.MediaType;
  25 +import org.springframework.test.context.ActiveProfiles;
  26 +import org.springframework.test.web.servlet.MockMvc;
  27 +import org.springframework.test.web.servlet.request.RequestPostProcessor;
  28 +
  29 +import java.util.List;
  30 +import java.util.Map;
  31 +
  32 +import static org.mockito.ArgumentMatchers.any;
  33 +import static org.mockito.ArgumentMatchers.anyString;
  34 +import static org.mockito.Mockito.when;
  35 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
  36 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
  37 +
  38 +@ActiveProfiles("test")
  39 +@WebMvcTest(controllers = UserController.class)
  40 +@Import({SecurityConfig.class, JwtAuthenticationFilter.class, BeanConfig.class, JwtUtil.class, JwtProperties.class})
  41 +class UserControllerTest {
  42 +
  43 + @Autowired private MockMvc mockMvc;
  44 + @Autowired private ObjectMapper objectMapper;
  45 + @Autowired private JwtUtil jwtUtil;
  46 + @MockBean private UserService userService;
  47 +
  48 + RequestPostProcessor superAdmin() {
  49 + String token = jwtUtil.generateAccessToken("u1", "admin", "超级管理员", "b1");
  50 + return request -> { request.addHeader("Authorization", "Bearer " + token); return request; };
  51 + }
  52 +
  53 + @Test
  54 + void createUser_noAuth_returns401() throws Exception {
  55 + mockMvc.perform(post("/api/usr/users")
  56 + .contentType(MediaType.APPLICATION_JSON)
  57 + .content("{}"))
  58 + .andExpect(status().isUnauthorized());
  59 + }
  60 +
  61 + @Test
  62 + void createUser_validRequest_returns200() throws Exception {
  63 + UserCreateRespVO resp = new UserCreateRespVO();
  64 + resp.setUserId("new-user-id");
  65 + resp.setUserCode("UC001");
  66 + resp.setUsername("testuser");
  67 + when(userService.createUser(any(), any())).thenReturn(resp);
  68 +
  69 + Map<String, Object> body = Map.of(
  70 + "userCode", "UC001",
  71 + "username", "testuser",
  72 + "userType", "普通用户",
  73 + "language", "中文");
  74 + mockMvc.perform(post("/api/usr/users")
  75 + .with(superAdmin())
  76 + .contentType(MediaType.APPLICATION_JSON)
  77 + .content(objectMapper.writeValueAsString(body)))
  78 + .andExpect(status().isOk())
  79 + .andExpect(jsonPath("$.code").value(200))
  80 + .andExpect(jsonPath("$.data.userId").value("new-user-id"));
  81 + }
  82 +
  83 + @Test
  84 + void createUser_missingUserCode_returns40001() throws Exception {
  85 + Map<String, Object> body = Map.of(
  86 + "username", "testuser",
  87 + "userType", "普通用户",
  88 + "language", "中文");
  89 + mockMvc.perform(post("/api/usr/users")
  90 + .with(superAdmin())
  91 + .contentType(MediaType.APPLICATION_JSON)
  92 + .content(objectMapper.writeValueAsString(body)))
  93 + .andExpect(status().isOk())
  94 + .andExpect(jsonPath("$.code").value(40001));
  95 + }
  96 +
  97 + @Test
  98 + void getStaffs_returns200() throws Exception {
  99 + StaffVO s = new StaffVO();
  100 + s.setSId("s1");
  101 + s.setSStaffName("张三");
  102 + when(userService.getStaffs(anyString())).thenReturn(List.of(s));
  103 +
  104 + mockMvc.perform(get("/api/usr/users/staffs").with(superAdmin()))
  105 + .andExpect(status().isOk())
  106 + .andExpect(jsonPath("$.code").value(200))
  107 + .andExpect(jsonPath("$.data[0].sId").value("s1"));
  108 + }
  109 +
  110 + @Test
  111 + void getUsers_withToken_returns200() throws Exception {
  112 + PageVO<UserListItemVO> page = new PageVO<>();
  113 + page.setTotal(0);
  114 + page.setPage(1);
  115 + page.setPageSize(20);
  116 + page.setList(List.of());
  117 + when(userService.getUserList(any(UserListQueryDTO.class), anyString())).thenReturn(page);
  118 +
  119 + mockMvc.perform(get("/api/usr/users").with(superAdmin()))
  120 + .andExpect(status().isOk())
  121 + .andExpect(jsonPath("$.code").value(200))
  122 + .andExpect(jsonPath("$.data.total").value(0))
  123 + .andExpect(jsonPath("$.data.list").isArray());
  124 + }
  125 +
  126 + @Test
  127 + void getUsers_noAuth_returns401() throws Exception {
  128 + mockMvc.perform(get("/api/usr/users"))
  129 + .andExpect(status().isUnauthorized());
  130 + }
  131 +
  132 + @Test
  133 + void updateUser_withToken_returns200() throws Exception {
  134 + UserUpdateRespVO resp = new UserUpdateRespVO();
  135 + resp.setUserId("u-target");
  136 + resp.setUsername("alice");
  137 + resp.setUpdatedAt(java.time.LocalDateTime.now());
  138 + when(userService.updateUser(anyString(), any(UserUpdateReqDTO.class), any())).thenReturn(resp);
  139 +
  140 + UserUpdateReqDTO body = new UserUpdateReqDTO();
  141 + body.setUserType("普通用户");
  142 + body.setLanguage("中文");
  143 + body.setPermGroupIds(List.of());
  144 +
  145 + mockMvc.perform(put("/api/usr/users/u-target").with(superAdmin())
  146 + .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
  147 + .content(objectMapper.writeValueAsString(body)))
  148 + .andExpect(status().isOk())
  149 + .andExpect(jsonPath("$.code").value(200))
  150 + .andExpect(jsonPath("$.data.userId").value("u-target"))
  151 + .andExpect(jsonPath("$.data.username").value("alice"));
  152 + }
  153 +
  154 + @Test
  155 + void updateUser_noAuth_returns401() throws Exception {
  156 + mockMvc.perform(put("/api/usr/users/u-target")
  157 + .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
  158 + .content("{}"))
  159 + .andExpect(status().isUnauthorized());
  160 + }
  161 +
  162 + @Test
  163 + void getPermissionGroups_returns200() throws Exception {
  164 + PermissionGroupVO g = new PermissionGroupVO();
  165 + g.setSId("g1");
  166 + g.setSGroupCode("usr:create");
  167 + g.setSGroupName("新增用户");
  168 + when(userService.getPermissionGroups(anyString())).thenReturn(List.of(g));
  169 +
  170 + mockMvc.perform(get("/api/usr/users/permission-groups").with(superAdmin()))
  171 + .andExpect(status().isOk())
  172 + .andExpect(jsonPath("$.code").value(200))
  173 + .andExpect(jsonPath("$.data[0].sGroupCode").value("usr:create"));
  174 + }
  175 +}
... ...
backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java 0 → 100644
  1 +package com.example.erp.module.usr;
  2 +
  3 +import com.baomidou.mybatisplus.core.metadata.IPage;
  4 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  5 +import com.example.erp.common.exception.BizException;
  6 +import com.example.erp.common.vo.PageVO;
  7 +import com.example.erp.config.UserPrincipal;
  8 +import com.example.erp.module.usr.dto.UserCreateReqDTO;
  9 +import com.example.erp.module.usr.dto.UserListQueryDTO;
  10 +import com.example.erp.module.usr.dto.UserUpdateReqDTO;
  11 +import com.example.erp.module.usr.vo.UserListItemVO;
  12 +import com.example.erp.module.usr.vo.UserUpdateRespVO;
  13 +import com.example.erp.module.usr.entity.StaffEntity;
  14 +import com.example.erp.module.usr.entity.UsrUserEntity;
  15 +import com.example.erp.module.usr.entity.UserPermissionEntity;
  16 +import com.example.erp.module.usr.mapper.PermissionGroupMapper;
  17 +import com.example.erp.module.usr.mapper.StaffMapper;
  18 +import com.example.erp.common.constants.UsrErrorCode;
  19 +import com.example.erp.module.usr.mapper.UserPermissionMapper;
  20 +import com.example.erp.module.usr.mapper.UsrUserMapper;
  21 +import com.example.erp.module.usr.service.impl.UserServiceImpl;
  22 +import com.example.erp.module.usr.vo.UserCreateRespVO;
  23 +import org.junit.jupiter.api.BeforeEach;
  24 +import org.junit.jupiter.api.Test;
  25 +import org.junit.jupiter.api.extension.ExtendWith;
  26 +import org.mockito.InjectMocks;
  27 +import org.mockito.Mock;
  28 +import org.mockito.junit.jupiter.MockitoExtension;
  29 +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  30 +
  31 +import java.util.List;
  32 +
  33 +import static org.junit.jupiter.api.Assertions.*;
  34 +import static org.mockito.ArgumentMatchers.*;
  35 +import static org.mockito.Mockito.*;
  36 +
  37 +@ExtendWith(MockitoExtension.class)
  38 +class UserServiceTest {
  39 +
  40 + @Mock private UsrUserMapper userMapper;
  41 + @Mock private StaffMapper staffMapper;
  42 + @Mock private PermissionGroupMapper permGroupMapper;
  43 + @Mock private UserPermissionMapper userPermissionMapper;
  44 + @Mock private BCryptPasswordEncoder passwordEncoder;
  45 +
  46 + @InjectMocks
  47 + private UserServiceImpl userService;
  48 +
  49 + private UserCreateReqDTO req;
  50 + private UserPrincipal superAdmin;
  51 + private UserPrincipal normalUser;
  52 +
  53 + @BeforeEach
  54 + void setUp() {
  55 + req = new UserCreateReqDTO();
  56 + req.setUserCode("UC001");
  57 + req.setUsername("testuser");
  58 + req.setUserType("普通用户");
  59 + req.setLanguage("中文");
  60 +
  61 + superAdmin = new UserPrincipal("u1", "admin", "超级管理员", "b1");
  62 + normalUser = new UserPrincipal("u2", "user", "普通用户", "b1");
  63 + }
  64 +
  65 + @Test
  66 + void createUser_normalUser_throws40300() {
  67 + BizException ex = assertThrows(BizException.class,
  68 + () -> userService.createUser(req, normalUser));
  69 + assertEquals(40300, ex.getCode());
  70 + }
  71 +
  72 + @Test
  73 + void createUser_success_insertsUserAndReturnsVO() {
  74 + when(userMapper.selectCount(any())).thenReturn(0L);
  75 + when(passwordEncoder.encode(anyString())).thenReturn("$2a$10$hashed");
  76 + when(userMapper.insert(any(UsrUserEntity.class))).thenReturn(1);
  77 +
  78 + UserCreateRespVO vo = userService.createUser(req, superAdmin);
  79 +
  80 + assertNotNull(vo.getUserId());
  81 + assertEquals("UC001", vo.getUserCode());
  82 + assertEquals("testuser", vo.getUsername());
  83 + verify(userMapper).insert(any(UsrUserEntity.class));
  84 + }
  85 +
  86 + @Test
  87 + void createUser_duplicateUserCode_throws40902() {
  88 + when(userMapper.selectCount(any())).thenReturn(1L);
  89 +
  90 + BizException ex = assertThrows(BizException.class,
  91 + () -> userService.createUser(req, superAdmin));
  92 + assertEquals(40902, ex.getCode());
  93 + }
  94 +
  95 + @Test
  96 + void createUser_duplicateUsername_throws40901() {
  97 + when(userMapper.selectCount(any())).thenReturn(0L, 1L);
  98 +
  99 + BizException ex = assertThrows(BizException.class,
  100 + () -> userService.createUser(req, superAdmin));
  101 + assertEquals(40901, ex.getCode());
  102 + }
  103 +
  104 + @Test
  105 + @SuppressWarnings("unchecked")
  106 + void createUser_withPermGroups_insertsPermissions() {
  107 + req.setPermGroupIds(List.of("g1", "g2"));
  108 + when(userMapper.selectCount(any())).thenReturn(0L);
  109 + when(passwordEncoder.encode(anyString())).thenReturn("$2a$10$hashed");
  110 + when(userMapper.insert(any(UsrUserEntity.class))).thenReturn(1);
  111 +
  112 + userService.createUser(req, superAdmin);
  113 +
  114 + verify(userPermissionMapper).insert(argThat((java.util.Collection<UserPermissionEntity> c) -> c.size() == 2));
  115 + }
  116 +
  117 + @Test
  118 + void getUserList_queryDtoDefaults() {
  119 + UserListQueryDTO q = new UserListQueryDTO();
  120 + assertEquals("username", q.getQueryField());
  121 + assertEquals("contains", q.getMatchType());
  122 + assertEquals(20, q.getPageSize());
  123 + assertEquals(1, q.getPage());
  124 + }
  125 +
  126 + @Test
  127 + void getUserList_capsPageSizeAt100_callsMapper() {
  128 + IPage<UserListItemVO> mockPage = new Page<>(1, 100, 5);
  129 + when(userMapper.selectUserList(argThat(p -> p.getSize() == 100), eq("b1"), any(), any(), any()))
  130 + .thenReturn(mockPage);
  131 +
  132 + UserListQueryDTO q = new UserListQueryDTO();
  133 + q.setPageSize(200);
  134 + PageVO<UserListItemVO> result = userService.getUserList(q, "b1");
  135 +
  136 + assertEquals(5L, result.getTotal());
  137 + verify(userMapper).selectUserList(argThat(p -> p.getSize() == 100), eq("b1"), any(), any(), any());
  138 + }
  139 +
  140 + @Test
  141 + void getUserList_emptyQueryValue_doesNotFilter() {
  142 + IPage<UserListItemVO> mockPage = new Page<>(1, 20, 2);
  143 + when(userMapper.selectUserList(any(), eq("b1"), eq("username"), eq("contains"), eq("")))
  144 + .thenReturn(mockPage);
  145 +
  146 + UserListQueryDTO q = new UserListQueryDTO();
  147 + q.setQueryValue(null);
  148 + PageVO<UserListItemVO> result = userService.getUserList(q, "b1");
  149 +
  150 + assertEquals(2L, result.getTotal());
  151 + }
  152 +
  153 + @Test
  154 + void createUser_invalidEmployeeId_throws40001() {
  155 + req.setEmployeeId("bad-staff-id");
  156 + when(userMapper.selectCount(any())).thenReturn(0L);
  157 + when(staffMapper.selectOne(any())).thenReturn(null);
  158 +
  159 + BizException ex = assertThrows(BizException.class,
  160 + () -> userService.createUser(req, superAdmin));
  161 + assertEquals(UsrErrorCode.EMPLOYEE_NOT_FOUND, ex.getCode());
  162 + }
  163 +
  164 + @Test
  165 + void updateUser_dtoDefaults() {
  166 + com.example.erp.module.usr.dto.UserUpdateReqDTO dto = new com.example.erp.module.usr.dto.UserUpdateReqDTO();
  167 + dto.setUserType("普通用户");
  168 + dto.setLanguage("中文");
  169 + dto.setCanEditDoc(false);
  170 + dto.setDisabled(false);
  171 + dto.setEmployeeId(null);
  172 + dto.setPermGroupIds(List.of());
  173 +
  174 + assertEquals("普通用户", dto.getUserType());
  175 + assertEquals("中文", dto.getLanguage());
  176 + assertFalse(dto.isCanEditDoc());
  177 + assertFalse(dto.isDisabled());
  178 + assertNull(dto.getEmployeeId());
  179 + assertTrue(dto.getPermGroupIds().isEmpty());
  180 +
  181 + com.example.erp.module.usr.vo.UserUpdateRespVO resp = new com.example.erp.module.usr.vo.UserUpdateRespVO();
  182 + resp.setUserId("u1");
  183 + resp.setUsername("alice");
  184 + resp.setUpdatedAt(java.time.LocalDateTime.now());
  185 + assertEquals("u1", resp.getUserId());
  186 + assertEquals("alice", resp.getUsername());
  187 + assertNotNull(resp.getUpdatedAt());
  188 +
  189 + assertEquals(40400, UsrErrorCode.USER_NOT_FOUND);
  190 + assertEquals(40301, UsrErrorCode.SELF_ADMIN_CHANGE);
  191 + }
  192 +
  193 + @Test
  194 + void updateUser_nonAdmin_throws40300() {
  195 + UserUpdateReqDTO dto = new UserUpdateReqDTO();
  196 + dto.setUserType("普通用户");
  197 + dto.setLanguage("中文");
  198 + dto.setPermGroupIds(List.of());
  199 +
  200 + BizException ex = assertThrows(BizException.class,
  201 + () -> userService.updateUser("u-target", dto, normalUser));
  202 + assertEquals(UsrErrorCode.PERMISSION_DENIED, ex.getCode());
  203 + }
  204 +
  205 + @Test
  206 + void updateUser_userNotFound_throws40400() {
  207 + UserUpdateReqDTO dto = new UserUpdateReqDTO();
  208 + dto.setUserType("普通用户");
  209 + dto.setLanguage("中文");
  210 + dto.setPermGroupIds(List.of());
  211 + when(userMapper.selectOne(any())).thenReturn(null);
  212 +
  213 + BizException ex = assertThrows(BizException.class,
  214 + () -> userService.updateUser("u-target", dto, superAdmin));
  215 + assertEquals(UsrErrorCode.USER_NOT_FOUND, ex.getCode());
  216 + }
  217 +
  218 + @Test
  219 + void updateUser_selfAdminChange_throws40301() {
  220 + UserUpdateReqDTO dto = new UserUpdateReqDTO();
  221 + dto.setUserType("普通用户");
  222 + dto.setLanguage("中文");
  223 + dto.setPermGroupIds(List.of());
  224 + UsrUserEntity existing = new UsrUserEntity();
  225 + existing.setSId("u1");
  226 + existing.setSUsername("admin");
  227 + when(userMapper.selectOne(any())).thenReturn(existing);
  228 +
  229 + // superAdmin.userId() == "u1", target userId == "u1" → self-admin change
  230 + BizException ex = assertThrows(BizException.class,
  231 + () -> userService.updateUser("u1", dto, superAdmin));
  232 + assertEquals(UsrErrorCode.SELF_ADMIN_CHANGE, ex.getCode());
  233 + }
  234 +
  235 + @Test
  236 + void updateUser_selfAdminKeepType_doesNotThrow() {
  237 + UserUpdateReqDTO dto = new UserUpdateReqDTO();
  238 + dto.setUserType("超级管理员");
  239 + dto.setLanguage("中文");
  240 + dto.setPermGroupIds(List.of());
  241 + UsrUserEntity existing = new UsrUserEntity();
  242 + existing.setSId("u1");
  243 + existing.setSUsername("admin");
  244 + when(userMapper.selectOne(any())).thenReturn(existing);
  245 + when(userMapper.update(any(), any())).thenReturn(1);
  246 +
  247 + // 自己保持 超级管理员 角色 → 不应抛 40301
  248 + assertDoesNotThrow(() -> userService.updateUser("u1", dto, superAdmin));
  249 + }
  250 +
  251 + @Test
  252 + void updateUser_invalidEmployee_throws40001() {
  253 + UserUpdateReqDTO dto = new UserUpdateReqDTO();
  254 + dto.setUserType("普通用户");
  255 + dto.setLanguage("中文");
  256 + dto.setEmployeeId("bad-emp");
  257 + dto.setPermGroupIds(List.of());
  258 + UsrUserEntity existing = new UsrUserEntity();
  259 + existing.setSId("u-target");
  260 + existing.setSUsername("alice");
  261 + when(userMapper.selectOne(any())).thenReturn(existing);
  262 + when(staffMapper.selectOne(any())).thenReturn(null);
  263 +
  264 + BizException ex = assertThrows(BizException.class,
  265 + () -> userService.updateUser("u-target", dto, superAdmin));
  266 + assertEquals(UsrErrorCode.EMPLOYEE_NOT_FOUND, ex.getCode());
  267 + }
  268 +
  269 + @Test
  270 + void updateUser_happyPath_updatesAndReturnsResp() {
  271 + UserUpdateReqDTO dto = new UserUpdateReqDTO();
  272 + dto.setUserType("超级管理员");
  273 + dto.setLanguage("英文");
  274 + dto.setCanEditDoc(true);
  275 + dto.setDisabled(false);
  276 + dto.setPermGroupIds(List.of("g1", "g2"));
  277 + UsrUserEntity existing = new UsrUserEntity();
  278 + existing.setSId("u-target");
  279 + existing.setSUsername("alice");
  280 + when(userMapper.selectOne(any())).thenReturn(existing);
  281 + when(userMapper.update(any(), any())).thenReturn(1);
  282 + when(userPermissionMapper.delete(any())).thenReturn(0);
  283 +
  284 + UserUpdateRespVO resp = userService.updateUser("u-target", dto, superAdmin);
  285 +
  286 + assertNotNull(resp);
  287 + assertEquals("u-target", resp.getUserId());
  288 + assertEquals("alice", resp.getUsername());
  289 + assertNotNull(resp.getUpdatedAt());
  290 + verify(userMapper).update(any(), any());
  291 + verify(userPermissionMapper).delete(any());
  292 + verify(userPermissionMapper).insert(argThat((java.util.Collection<UserPermissionEntity> c) -> c.size() == 2));
  293 + }
  294 +
  295 + @Test
  296 + void updateUser_emptyPermGroupIds_onlyDeletes() {
  297 + UserUpdateReqDTO dto = new UserUpdateReqDTO();
  298 + dto.setUserType("普通用户");
  299 + dto.setLanguage("中文");
  300 + dto.setPermGroupIds(List.of());
  301 + UsrUserEntity existing = new UsrUserEntity();
  302 + existing.setSId("u-target");
  303 + existing.setSUsername("bob");
  304 + when(userMapper.selectOne(any())).thenReturn(existing);
  305 + when(userMapper.update(any(), any())).thenReturn(1);
  306 + when(userPermissionMapper.delete(any())).thenReturn(0);
  307 +
  308 + userService.updateUser("u-target", dto, superAdmin);
  309 +
  310 + verify(userPermissionMapper).delete(any());
  311 + verify(userPermissionMapper, never()).insert(anyList());
  312 + }
  313 +}
... ...
backend/src/test/resources/application-test.yml 0 → 100644
  1 +spring:
  2 + datasource:
  3 + url: jdbc:mysql://${DB_HOST:118.178.19.35}:${DB_PORT:3318}/${DB_SCHEMA:xlyweberp_vibe_erp_test}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=UTF-8
  4 + username: ${DB_USER:xlyprint}
  5 + password: ${DB_PASSWORD:xlyXLYprint2016}
  6 + driver-class-name: com.mysql.cj.jdbc.Driver
  7 + flyway:
  8 + baseline-on-migrate: true
  9 + baseline-version: 1
  10 + validate-on-migrate: false
  11 +
  12 +jwt:
  13 + secret: ${JWT_SECRET:a3b7e8f1c4d6029e5b8f37a1c9d2e4068b5f1d3a7c0e9b2f48d6a1c5e7f9b3d2}
  14 + access-token-expiry: 86400
  15 + refresh-token-expiry: 604800
  16 +
  17 +logging:
  18 + level:
  19 + com.example.erp: DEBUG
... ...
docs/03-数据库设计文档.md
... ... @@ -64,7 +64,7 @@ usr_user(用户主表)
64 64  
65 65 ### 索引
66 66  
67   -- `uk_usr_user_username` (UNIQUE): `sUsername` — 全局唯一约束
  67 +- `uk_usr_user_username_tenant` (UNIQUE): `(sUsername, sBrandsId)` — 用户名在同一 brand 内唯一(V2 迁移:原全局唯一改为多租户复合唯一)
68 68 - `uk_usr_user_usercode` (UNIQUE): `sUserCode` — 用户号唯一约束
69 69 - `idx_usr_user_tenant` (INDEX): `sBrandsId, sSubsidiaryId` — 多租户隔离查询
70 70 - `idx_usr_user_type` (INDEX): `sUserType` — 按用户类型过滤
... ...
docs/04-技术规范.md
... ... @@ -147,8 +147,8 @@ type 取值:`feat` / `fix` / `refactor` / `test` / `docs` / `chore`
147 147  
148 148 ### 3.2 分页查询
149 149  
150   -- **入参**:`pageNum`(从 1 起)、`pageSize`(默认 20,最大 100)
151   -- **出参**:`{ list: [], total: N, pageNum: N, pageSize: N }`
  150 +- **入参**:`page`(从 1 起)、`pageSize`(默认 20,最大 100)
  151 +- **出参**:`{ list: [], total: N, page: N, pageSize: N }`
152 152 - 后端使用 MyBatis-Plus `Page<T>` 对象;前端使用 Ant Design `Table` 的 `pagination` 属性
153 153  
154 154 ### 3.3 日期与金额
... ...
docs/05-API接口契约.md
... ... @@ -24,7 +24,7 @@ BasePath: `/api`
24 24 除登录接口外,所有接口均需在请求头携带 `Authorization: Bearer <access_token>`。Token 由 REQ-USR-004 登录接口签发,有效期 24 小时。
25 25  
26 26 ### 分页参数
27   -列表查询接口统一使用以下分页入参:`pageNum`(页码,从 1 起)、`pageSize`(每页条数,默认 20,最大 100)。响应体 `data` 字段格式:`{ list: [], total: N, pageNum: N, pageSize: N }`。
  27 +列表查询接口统一使用以下分页入参:`page`(页码,从 1 起)、`pageSize`(每页条数,默认 20,最大 100)。响应体 `data` 字段格式:`{ list: [], total: N, page: N, pageSize: N }`。
28 28  
29 29 ## 接口清单
30 30 (各模块接口段落见下方,由 `downstream-gen` 按 REQ 填入)
... ... @@ -73,8 +73,8 @@ BasePath: `/api`
73 73 - **Path**: `/api/usr/users`
74 74 - **Auth**: Bearer Token
75 75 - **Permission**: `usr:query`
76   -- **请求**: Query 参数:`field=用户名|员工名|用户号|部门|用户类型|作废|登录日期|制单人`、`matchMode=包含|不包含|等于`、`value=string`、`pageNum=int`、`pageSize=int`
77   -- **响应**: `{ list: [{ "userId", "username", "staffName", "userCode", "department", "userType", "language", "isDisabled", "lastLoginDate", "creatorUsername", "createDate" }], total, pageNum, pageSize }` — 密码字段不返回
  76 +- **请求**: Query 参数:`queryField=username|staffName|userCode|department|userType|disabled|lastLoginDate|creator`(默认 username)、`matchType=contains|notContains|equals`(默认 contains)、`queryValue=string`、`page=int`(默认 1)、`pageSize=int`(默认 20,最大 100)
  77 +- **响应**: `{ list: [{ "sId", "sUsername", "sUserCode", "sUserType", "sLanguage", "bIsDisabled", "tLastLoginDate", "sCreatorUsername", "tCreateDate", "sStaffName", "sDepartment" }], total, page, pageSize }` — sPasswordHash / iLoginFailCount / tLockUntil 不返回
78 78  
79 79 #### 错误码
80 80 - `40001` — 参数校验失败
... ...
docs/08-模块任务管理.md
... ... @@ -58,9 +58,9 @@
58 58 - module_usr 用户管理
59 59 - 依赖: —
60 60 - 路径: backend/module/usr/, frontend/pages/usr/
61   - - MR:
  61 + - MR: !1
62 62 - 功能:
63   - - [ ] REQ-USR-004 用户登录
64   - - [ ] REQ-USR-001 增加用户
65   - - [ ] REQ-USR-003 查询用户
66   - - [ ] REQ-USR-002 修改用户
  63 + - [x] REQ-USR-004 用户登录
  64 + - [x] REQ-USR-001 增加用户
  65 + - [x] REQ-USR-003 查询用户
  66 + - [x] REQ-USR-002 修改用户
... ...
docs/superpowers/module-reports/2026-05-08-module_usr.md 0 → 100644
  1 +---
  2 +module_id: module_usr
  3 +date: 2026-05-08
  4 +git_range: master..module-module_usr
  5 +---
  6 +
  7 +# 模块完成报告 — module_usr 用户管理
  8 +
  9 +## ① 模块信息
  10 +- 模块 ID: module_usr
  11 +- 模块名: 用户管理
  12 +- 开发区间: master..module-module_usr(共 29 commits)
  13 +
  14 +## ② REQ 完成清单
  15 +
  16 +- [x] REQ-USR-004 — 用户登录
  17 + - spec: docs/superpowers/specs/2026-05-08-REQ-USR-004.md
  18 + - plan: docs/superpowers/plans/2026-05-08-REQ-USR-004.md
  19 + - review: docs/superpowers/reviews/2026-05-08-REQ-USR-004.md
  20 +
  21 +- [x] REQ-USR-001 — 增加用户
  22 + - spec: docs/superpowers/specs/2026-05-08-REQ-USR-001.md
  23 + - plan: docs/superpowers/plans/2026-05-08-REQ-USR-001.md
  24 + - review: docs/superpowers/reviews/2026-05-08-REQ-USR-001.md
  25 +
  26 +- [x] REQ-USR-003 — 查询用户
  27 + - spec: docs/superpowers/specs/2026-05-08-REQ-USR-003.md
  28 + - plan: docs/superpowers/plans/2026-05-08-REQ-USR-003.md
  29 + - review: docs/superpowers/reviews/2026-05-08-REQ-USR-003.md
  30 +
  31 +- [x] REQ-USR-002 — 修改用户
  32 + - spec: docs/superpowers/specs/2026-05-08-REQ-USR-002.md
  33 + - plan: docs/superpowers/plans/2026-05-08-REQ-USR-002.md
  34 + - review: docs/superpowers/reviews/2026-05-08-REQ-USR-002.md
  35 +
  36 +## ③ 文件变更表
  37 +
  38 +| 文件 | 操作 | 说明 |
  39 +|---|---|---|
  40 +| backend/pom.xml | 新增 | Spring Boot 3 项目配置,含 checkstyle 插件 |
  41 +| backend/checkstyle.xml | 新增 | 项目 checkstyle 规则(Spring Boot 友好) |
  42 +| backend/src/main/java/com/example/erp/common/\* | 新增 | Result/BizException/GlobalExceptionHandler/JwtUtil/PageVO/错误码常量 |
  43 +| backend/src/main/java/com/example/erp/config/\* | 新增 | SecurityConfig/JwtFilter/JwtProperties/UserPrincipal/BeanConfig/MybatisPlusConfig |
  44 +| backend/src/main/java/com/example/erp/module/usr/controller/\* | 新增 | AuthController(登录/刷新/品牌列表)、UserController(CRUD 接口) |
  45 +| backend/src/main/java/com/example/erp/module/usr/service/\* | 新增 | AuthService/AuthServiceImpl/UserService/UserServiceImpl |
  46 +| backend/src/main/java/com/example/erp/module/usr/entity/\* | 新增 | BrandEntity/UsrUserEntity/StaffEntity/PermissionGroupEntity/UserPermissionEntity |
  47 +| backend/src/main/java/com/example/erp/module/usr/mapper/\* | 新增 | BrandMapper/UsrUserMapper/StaffMapper/PermissionGroupMapper/UserPermissionMapper |
  48 +| backend/src/main/java/com/example/erp/module/usr/dto/\* | 新增 | LoginReqDTO/RefreshTokenReqDTO/UserCreateReqDTO/UserListQueryDTO/UserUpdateReqDTO |
  49 +| backend/src/main/java/com/example/erp/module/usr/vo/\* | 新增 | LoginVO/BrandVO/StaffVO/PermissionGroupVO/UserCreateRespVO/UserListItemVO/UserUpdateRespVO |
  50 +| backend/src/main/resources/mapper/UsrUserMapper.xml | 新增 | selectUserList 动态查询 SQL |
  51 +| backend/src/main/resources/db/migration/V1\_\_initial\_schema.sql | 新增 | 初始全量 schema |
  52 +| backend/src/main/resources/db/migration/V2\_\_fix\_username\_unique\_per\_tenant.sql | 新增 | 用户名唯一索引改为租户范围内唯一 |
  53 +| backend/src/test/\* | 新增 | 49 个测试(ApplicationContext/JwtUtil/Result/AuthService/AuthController/UserService/UserController/BrandMapper) |
  54 +| frontend/src/api/auth.ts | 新增 | 登录/刷新 API 函数 |
  55 +| frontend/src/api/request.ts | 新增 | Axios 请求拦截器(含 JWT 自动注入) |
  56 +| frontend/src/api/usr.ts | 新增 | 用户增/查/改 API 接口及类型定义 |
  57 +| frontend/src/pages/usr/LoginPage.tsx | 新增 | 登录页(多品牌选择) |
  58 +| frontend/src/pages/usr/UserFormDrawer.tsx | 新增 | 新增/修改用户抽屉(复用组件) |
  59 +| frontend/src/pages/usr/UserListPage.tsx | 新增 | 用户列表(搜索+分页+操作列) |
  60 +| frontend/src/store/\* | 新增 | Redux auth 切片(credentials/userInfo) |
  61 +| frontend/src/components/PermButton.tsx | 新增 | 基于用户类型的权限按钮组件 |
  62 +| frontend/src/test/\* | 新增 | 10 个前端测试(authSlice/LoginPage/UserListPage) |
  63 +| frontend/package.json | 修改 | lint 脚本改为 tsc --noEmit(eslint 未安装) |
  64 +| docs/03-数据库设计文档.md | 修改 | 同步 V2 migration 唯一索引变更 |
  65 +| docs/04-技术规范.md | 修改 | 分页入参规范统一为 page(原 pageNum) |
  66 +| docs/05-API接口契约.md | 修改 | 分页规范同步更新 |
  67 +| docs/08-模块任务管理.md | 修改 | 4 个 REQ 全部勾选 [x] |
  68 +| scripts/test.sh | 修改 | 添加 Java 21 PATH 适配(Lombok 兼容性) |
  69 +
  70 +## ④ 数据库使用表
  71 +- 读: `brand`(登录品牌列表)、`tStaff`(员工下拉)、`usr_permission_group`(权限组下拉)
  72 +- 写: `usr_user`(登录时更新 tLastLoginDate/iLoginFailCount/tLockUntil;新增/修改用户字段)、`usr_user_permission`(新增/修改时先删后插)
  73 +
  74 +## ⑤ 测试结果
  75 +- `scripts/test.sh` 最终:green
  76 +- 通过: 59 / 失败: 0 / 跳过: 0
  77 +- 覆盖率: 未配置覆盖率工具(所有业务路径已有对应测试用例)
  78 +
  79 +## ⑥ 本模块新增 Migration
  80 +
  81 +- `backend/src/main/resources/db/migration/V1__initial_schema.sql` — 全量初始 schema(usr_user / tStaff / usr_permission_group / usr_user_permission / brand 五张表)
  82 +- `backend/src/main/resources/db/migration/V2__fix_username_unique_per_tenant.sql` — 将 uk_usr_user_username 从全局唯一改为租户范围内唯一(允许不同 brand 使用相同用户名)
  83 +
  84 +## ⑦ 跨模块改动清单(软规则 S2)
  85 +
  86 +本项目当前仅含 module_usr 一个模块,无跨模块改动。
  87 +
  88 +改动了两处项目级规范文档:
  89 +- `docs/04-技术规范.md § 3.2`:分页入参统一为 `page`(REQ-USR-003 review 发现 `pageNum` 与实现不一致,修正文档)
  90 +- `docs/05-API接口契约.md`:分页规范同步更新
  91 +
  92 +## ⑧ 偏离 spec 清单
  93 +
  94 +- REQ-USR-002:`UserListPage` 传给编辑抽屉的 `initialData` 中 `permGroupIds` 字段未传值(始终为空数组)。原因:用户列表 API 不返回每用户的权限组关联,无法在列表页预填。规格 § 前端交互设计 line 103 已明确说明"初始化为空数组即可,用户可重新勾选",属有意决策而非实现缺陷。
  95 +
  96 +## ⑨ AI reviewer 报告汇总
  97 +
  98 +- REQ-USR-004: round 2 — approve(link: docs/superpowers/reviews/2026-05-08-REQ-USR-004.md)
  99 +- REQ-USR-001: round 2 — approve(link: docs/superpowers/reviews/2026-05-08-REQ-USR-001.md)
  100 +- REQ-USR-003: round 4 — approve(link: docs/superpowers/reviews/2026-05-08-REQ-USR-003.md)
  101 +- REQ-USR-002: round 2 — approve(link: docs/superpowers/reviews/2026-05-08-REQ-USR-002.md)
  102 +
  103 +## ⑩ 已知问题
  104 +
  105 +1. **前端 lint 工具缺失**:`eslint` 未在 `package.json` 中声明为 devDependency,lint 脚本临时改为 `tsc --noEmit`。建议后续补充 ESLint + TypeScript ESLint 插件配置。
  106 +2. **`updatedAt` 序列化格式**:`UserUpdateRespVO.updatedAt` 未加 `@JsonFormat` 注解,依赖 Jackson 全局配置。如未配置全局 LocalDateTime 序列化器,返回值可能为数组格式而非 ISO-8601 字符串。
  107 +3. **controller 层 BizException 错误码映射测试缺失**:`UserControllerTest` 未覆盖 40300/40400/40301 通过 `GlobalExceptionHandler` 映射为 JSON 错误码的路径(低风险,handler 已通过 createUser 路径间接覆盖)。
  108 +
  109 +## ⑪ 下一模块预览
  110 +
  111 +当前项目 `docs/02-开发计划.md` 仅包含 module_usr 一个模块,无后续模块。本次开发计划全部完成。
  112 +
  113 +## ⑫ MR 链接
  114 +
  115 +http://git.xlyprint.cn/zhuzc/test3/-/merge_requests/1
... ...
docs/superpowers/module-reports/module_usr-test-gate.md 0 → 100644
  1 +## Local test gate — module_usr
  2 +
  3 +执行时间: 2026-05-08T12:40:11+08:00
  4 +
  5 +### scripts/test.sh (subagent)
  6 +- 子会话: a5ec519c45282bef8
  7 +- 命令: ./scripts/test.sh
  8 +- 退出码: 0
  9 +- 通过: 59 / 失败: 0
  10 +- 关键 stdout (≤30 行):
  11 +
  12 +```
  13 +[test.sh] 1/6 setup test db
  14 +[test.sh] 2/6 build
  15 +[test.sh] 3/6 lint
  16 + Backend: 0 Checkstyle violations — BUILD SUCCESS
  17 + Frontend: tsc --noEmit (0 errors)
  18 +[test.sh] 4/6 unit + integration
  19 + Backend: Tests run: 49, Failures: 0, Errors: 0, Skipped: 0
  20 + Frontend: Test Files 3 passed (3), Tests 10 passed (10)
  21 +[test.sh] 5/6 E2E
  22 +[test.sh] e2e 略
  23 +[test.sh] 6/6 reset test db
  24 +[test.sh] GREEN
  25 +```
  26 +
  27 +结论: green
... ...
docs/superpowers/plans/2026-05-08-REQ-USR-001.md 0 → 100644
  1 +# REQ-USR-001 增加用户 Implementation Plan
  2 +
  3 +> **Execution:** Parent skill `feature-tdd` executes this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
  4 +
  5 +**Goal:** 超级管理员通过 POST /api/usr/users 新建用户账号,系统自动初始化密码、写入权限关联;前端提供「用户管理」页+新增 Drawer 表单。
  6 +
  7 +**Architecture:** Spring Boot 后端新增 UserController(三个端点:createUser / getStaffs / getPermissionGroups)+ UserServiceImpl(业务逻辑+事务)+ 三个辅助 Entity/Mapper(StaffEntity、PermissionGroupEntity、UserPermissionEntity)。JwtAuthenticationFilter 升级为存储 UserPrincipal record,使 Controller 通过 @AuthenticationPrincipal 取到 brandId / username / userType。前端新增 api/usr.ts + UserListPage.tsx + UserFormDrawer.tsx,在 App.tsx 补充 /usr/users 路由。
  8 +
  9 +**Tech Stack:** Spring Boot 3.3.5 + MyBatis-Plus 3.5.7 + Lombok + Spring Security + spring-security-test;React 18 + Ant Design 5 + Vitest + @testing-library/react
  10 +
  11 +---
  12 +
  13 +## 文件映射
  14 +
  15 +**新建**:
  16 +- `backend/.../config/UserPrincipal.java` — JWT principal record(userId, username, userType, brandId)
  17 +- `backend/.../module/usr/entity/StaffEntity.java` — 映射 tStaff
  18 +- `backend/.../module/usr/entity/PermissionGroupEntity.java` — 映射 usr_permission_group
  19 +- `backend/.../module/usr/entity/UserPermissionEntity.java` — 映射 usr_user_permission
  20 +- `backend/.../module/usr/mapper/StaffMapper.java`
  21 +- `backend/.../module/usr/mapper/PermissionGroupMapper.java`
  22 +- `backend/.../module/usr/mapper/UserPermissionMapper.java`
  23 +- `backend/.../common/constants/UsrErrorCode.java` — 40300/40901/40902
  24 +- `backend/.../module/usr/dto/UserCreateReqDTO.java`
  25 +- `backend/.../module/usr/vo/UserCreateRespVO.java`
  26 +- `backend/.../module/usr/vo/StaffVO.java`
  27 +- `backend/.../module/usr/vo/PermissionGroupVO.java`
  28 +- `backend/.../module/usr/service/UserService.java`
  29 +- `backend/.../module/usr/service/impl/UserServiceImpl.java`
  30 +- `backend/.../module/usr/controller/UserController.java`
  31 +- `backend/.../module/usr/UserServiceTest.java`
  32 +- `backend/.../module/usr/UserControllerTest.java`
  33 +- `frontend/src/api/usr.ts`
  34 +- `frontend/src/pages/usr/UserListPage.tsx`
  35 +- `frontend/src/pages/usr/UserFormDrawer.tsx`
  36 +- `frontend/src/test/UserListPage.test.tsx`
  37 +
  38 +**修改**:
  39 +- `backend/.../config/JwtAuthenticationFilter.java` — 存 UserPrincipal 而非 String(CLAUDE.md S2,必要基础设施)
  40 +- `frontend/src/App.tsx` — 补 /usr/users 路由
  41 +
  42 +---
  43 +
  44 +## 合同级常量
  45 +
  46 +**UsrErrorCode**:
  47 +- `PERMISSION_DENIED = 40300`
  48 +- `USERNAME_EXISTS = 40901`
  49 +- `USER_CODE_EXISTS = 40902`
  50 +
  51 +**UsrErrorCode 用法**:`throw new BizException(UsrErrorCode.PERMISSION_DENIED, "权限不足")`
  52 +
  53 +**UserPrincipal(Java record)**:
  54 +```java
  55 +package com.example.erp.config;
  56 +public record UserPrincipal(String userId, String username, String userType, String brandId) {}
  57 +```
  58 +
  59 +**JWT claim 名**(来自 JwtUtil.generateAccessToken):
  60 +- subject → userId
  61 +- `"username"` → username
  62 +- `"userType"` → userType
  63 +- `"brandId"` → brandId
  64 +
  65 +---
  66 +
  67 +### Task 1: UserPrincipal + JwtAuthenticationFilter 升级(跨模块基础设施 S2)
  68 +
  69 +**Files:**
  70 +- Create: `backend/src/main/java/com/example/erp/config/UserPrincipal.java`
  71 +- Modify: `backend/src/main/java/com/example/erp/config/JwtAuthenticationFilter.java`
  72 +
  73 +**API shape:**
  74 +- `record UserPrincipal(String userId, String username, String userType, String brandId) {}`
  75 +- `JwtAuthenticationFilter.doFilterInternal()` — 解析 claims 后构造 `UserPrincipal`,存入 `UsernamePasswordAuthenticationToken` 的 principal
  76 +
  77 +- [ ] **Step 1: 写失败测试(在 UserControllerTest 中)**
  78 + - 文件:`backend/src/test/java/com/example/erp/module/usr/UserControllerTest.java`
  79 + - 仅写类骨架 + 一个空测试方法 `createUser_noToken_returns401()`,注解 `@WebMvcTest(controllers = UserController.class)`
  80 + - 此时编译失败(UserController 不存在)→ 子会话确认 FAIL
  81 +
  82 +- [ ] **Step 2: 实现 UserPrincipal + 修改 JwtAuthenticationFilter**
  83 + - 创建 `UserPrincipal.java` record(见合同级常量)
  84 + - 修改 `JwtAuthenticationFilter.doFilterInternal()`:
  85 + ```java
  86 + UserPrincipal principal = new UserPrincipal(
  87 + claims.getSubject(),
  88 + claims.get("username", String.class),
  89 + claims.get("userType", String.class),
  90 + claims.get("brandId", String.class)
  91 + );
  92 + UsernamePasswordAuthenticationToken auth =
  93 + new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList());
  94 + SecurityContextHolder.getContext().setAuthentication(auth);
  95 + ```
  96 +
  97 +- [ ] **Step 3: 子会话验证已有测试仍通过**
  98 + - 命令:`JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home mvn test -pl backend -Dtest=AuthControllerTest,AuthServiceTest`
  99 + - 期待:全部 PASS
  100 +
  101 +- [ ] **Step 4: Commit**
  102 + - `git add backend/src/main/java/com/example/erp/config/UserPrincipal.java backend/src/main/java/com/example/erp/config/JwtAuthenticationFilter.java`
  103 + - `git commit -m "feat(usr): UserPrincipal record + JwtFilter升级存principal REQ-USR-001"`
  104 +
  105 +---
  106 +
  107 +### Task 2: 实体 + Mapper + 错误码 + DTO/VO 骨架
  108 +
  109 +**Files:**
  110 +- Create: `backend/src/main/java/com/example/erp/module/usr/entity/StaffEntity.java`
  111 +- Create: `backend/src/main/java/com/example/erp/module/usr/entity/PermissionGroupEntity.java`
  112 +- Create: `backend/src/main/java/com/example/erp/module/usr/entity/UserPermissionEntity.java`
  113 +- Create: `backend/src/main/java/com/example/erp/module/usr/mapper/StaffMapper.java`
  114 +- Create: `backend/src/main/java/com/example/erp/module/usr/mapper/PermissionGroupMapper.java`
  115 +- Create: `backend/src/main/java/com/example/erp/module/usr/mapper/UserPermissionMapper.java`
  116 +- Create: `backend/src/main/java/com/example/erp/common/constants/UsrErrorCode.java`
  117 +- Create: `backend/src/main/java/com/example/erp/module/usr/dto/UserCreateReqDTO.java`
  118 +- Create: `backend/src/main/java/com/example/erp/module/usr/vo/UserCreateRespVO.java`
  119 +- Create: `backend/src/main/java/com/example/erp/module/usr/vo/StaffVO.java`
  120 +- Create: `backend/src/main/java/com/example/erp/module/usr/vo/PermissionGroupVO.java`
  121 +
  122 +**StaffEntity 字段**(来自 docs/03 tStaff 表):`iIncrement(PK)`, `sId`, `sBrandsId`, `sSubsidiaryId`, `tCreateDate`, `sStaffNo`, `sStaffName`, `sDepartment`, `sCreatedBy`, `bDeleted(Bit/Integer)`, `tDeletedDate`, `sDeletedBy`;`@TableName("tStaff")`
  123 +
  124 +**PermissionGroupEntity 字段**:`iIncrement(PK)`, `sId`, `sBrandsId`, `sSubsidiaryId`, `tCreateDate`, `sGroupCode`, `sGroupName`, `sCategory`;`@TableName("usr_permission_group")`
  125 +
  126 +**UserPermissionEntity 字段**:`iIncrement(PK)`, `sId`, `sBrandsId`, `sSubsidiaryId`, `tCreateDate`, `sUserId`, `sPermGroupId`;`@TableName("usr_user_permission")`
  127 +
  128 +**UserCreateReqDTO 字段**(`@Valid` 注解):
  129 +```java
  130 +@NotBlank String userCode;
  131 +@NotBlank String username;
  132 +@NotBlank @Pattern(regexp = "普通用户|超级管理员") String userType;
  133 +@NotBlank @Pattern(regexp = "中文|英文|繁体") String language;
  134 +boolean canEditDoc = false;
  135 +String employeeId; // nullable
  136 +List<String> permGroupIds; // nullable
  137 +```
  138 +
  139 +**UserCreateRespVO**:`String userId, userCode, username`
  140 +**StaffVO**:`String sId, sStaffName`
  141 +**PermissionGroupVO**:`String sId, sGroupCode, sGroupName, sCategory`
  142 +
  143 +- [ ] **Step 1: 写失败测试**
  144 + - 在 `UserServiceTest.java` 中写类骨架 + 一个空测试 `createUser_normalUser_throws40300()`,引用 `UserService`(未创建)
  145 + - 子会话确认编译 FAIL
  146 +
  147 +- [ ] **Step 2: 创建所有文件**
  148 + - 按上方规格创建所有 entity / mapper / errorcode / dto / vo 文件
  149 + - 每个 mapper 继承 `BaseMapper<T>`
  150 + - 每个 entity 使用 Lombok `@Getter @Setter`
  151 +
  152 +- [ ] **Step 3: 子会话验证编译通过**
  153 + - 命令:`JAVA_HOME=... mvn compile -pl backend`
  154 +
  155 +- [ ] **Step 4: Commit**
  156 + - `git add backend/src/main/java/`
  157 + - `git commit -m "feat(usr): 实体/Mapper/DTO/VO骨架 + UsrErrorCode REQ-USR-001"`
  158 +
  159 +---
  160 +
  161 +### Task 3: UserServiceImpl — createUser 业务逻辑
  162 +
  163 +**Files:**
  164 +- Create: `backend/src/main/java/com/example/erp/module/usr/service/UserService.java`
  165 +- Create: `backend/src/main/java/com/example/erp/module/usr/service/impl/UserServiceImpl.java`
  166 +- Test: `backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java`
  167 +
  168 +**API shape:**
  169 +- `UserService#createUser(UserCreateReqDTO req, UserPrincipal principal) : UserCreateRespVO`
  170 +- `UserService#getStaffs(String brandId) : List<StaffVO>`
  171 +- `UserService#getPermissionGroups(String brandId) : List<PermissionGroupVO>`
  172 +
  173 +**createUser 逻辑序列**:
  174 +1. `!"超级管理员".equals(principal.userType())` → `throw new BizException(40300, "权限不足")`
  175 +2. `userMapper.selectCount(LQ...eq(sUserCode, req.getUserCode())) > 0` → `throw new BizException(40902, "用户号已存在")`
  176 +3. `userMapper.selectCount(LQ...eq(sUsername).eq(sBrandsId)) > 0` → `throw new BizException(40901, "用户名已存在")`
  177 +4. `req.getEmployeeId() != null` → `staffMapper.selectOne(LQ...eq(sId).eq(sBrandsId)) == null` → `throw new BizException(40001, "员工不存在")`
  178 +5. 构造 `UsrUserEntity`:sId=UUID,sBrandsId=principal.brandId(),sCreatorUsername=principal.username(),tCreateDate=now,sPasswordHash=passwordEncoder.encode("666666"),bIsDisabled=0,iLoginFailCount=0
  179 +6. `userMapper.insert(user)`
  180 +7. `permGroupIds` 非 null 且非空 → 循环 `userPermissionMapper.insert(new UserPermissionEntity(UUID, brandId, now, user.sId, groupId))`
  181 +8. 返回 `UserCreateRespVO(user.sId, user.sUserCode, user.sUsername)`
  182 +
  183 +**UserServiceImpl 依赖**:`@RequiredArgsConstructor`;注入 `UsrUserMapper`、`StaffMapper`、`PermissionGroupMapper`、`UserPermissionMapper`、`BCryptPasswordEncoder`
  184 +
  185 +- [ ] **Step 1: 写失败测试**
  186 + - `UserServiceTest.java` 完整写入下列测试(全部引用 `UserService` 接口),子会话确认失败(UserService 接口不存在):
  187 + - `createUser_normalUser_throws40300` — principal.userType=普通用户 → BizException(40300)
  188 + - `createUser_success_insertsUserAndReturnsVO` — all mocks pass → 验证 userMapper.insert 被调用,返回 VO 有 userId
  189 + - `createUser_duplicateUserCode_throws40902` — selectCount 第1次返回 1L → BizException(40902)
  190 + - `createUser_duplicateUsername_throws40901` — selectCount 第1次返回 0L,第2次返回 1L → BizException(40901)
  191 + - `createUser_withPermGroups_insertsPermissions` — permGroupIds=["g1","g2"] → verify userPermissionMapper.insert 被调用 2 次
  192 + - `createUser_invalidEmployeeId_throws40001` — staffMapper.selectOne 返回 null → BizException(40001)
  193 +
  194 +- [ ] **Step 2: 实现 UserService + UserServiceImpl**
  195 + - 创建 `UserService.java` 接口(三个方法签名)
  196 + - 创建 `UserServiceImpl.java`,`@Service @RequiredArgsConstructor`,按逻辑序列实现 `createUser()`
  197 + - `getStaffs(brandId)`: `staffMapper.selectList(LQ...eq(sBrandsId, brandId).eq(bDeleted, 0).select("sId","sStaffName"))` → 映射 `StaffVO`
  198 + - `getPermissionGroups(brandId)`: `permGroupMapper.selectList(LQ...eq(sBrandsId, brandId))` → 映射 `PermissionGroupVO`(如 brandId 空则 selectList(null))
  199 +
  200 +- [ ] **Step 3: 子会话验证 UserServiceTest 全部通过**
  201 + - 命令:`JAVA_HOME=... mvn test -pl backend -Dtest=UserServiceTest`
  202 +
  203 +- [ ] **Step 4: Commit**
  204 + - `git add backend/src/`
  205 + - `git commit -m "feat(usr): UserServiceImpl.createUser + getStaffs + getPermissionGroups REQ-USR-001"`
  206 +
  207 +---
  208 +
  209 +### Task 4: UserController — 三个端点
  210 +
  211 +**Files:**
  212 +- Create: `backend/src/main/java/com/example/erp/module/usr/controller/UserController.java`
  213 +- Test: `backend/src/test/java/com/example/erp/module/usr/UserControllerTest.java`
  214 +
  215 +**API shape:**
  216 +```java
  217 +@RestController
  218 +@RequestMapping("/api/usr")
  219 +@RequiredArgsConstructor
  220 +public class UserController {
  221 + private final UserService userService;
  222 +
  223 + @PostMapping("/users")
  224 + public Result<UserCreateRespVO> createUser(
  225 + @Valid @RequestBody UserCreateReqDTO req,
  226 + @AuthenticationPrincipal UserPrincipal principal) { ... }
  227 +
  228 + @GetMapping("/users/staffs")
  229 + public Result<List<StaffVO>> getStaffs(@AuthenticationPrincipal UserPrincipal principal) { ... }
  230 +
  231 + @GetMapping("/users/permission-groups")
  232 + public Result<List<PermissionGroupVO>> getPermissionGroups(@AuthenticationPrincipal UserPrincipal principal) { ... }
  233 +}
  234 +```
  235 +
  236 +**测试助手** — `SecurityMockMvcRequestPostProcessors.authentication(...)` 注入 UserPrincipal:
  237 +```java
  238 +static RequestPostProcessor superAdmin() {
  239 + return authentication(new UsernamePasswordAuthenticationToken(
  240 + new UserPrincipal("u1","admin","超级管理员","b1"), null, emptyList()));
  241 +}
  242 +static RequestPostProcessor normalUser() {
  243 + return authentication(new UsernamePasswordAuthenticationToken(
  244 + new UserPrincipal("u2","user","普通用户","b1"), null, emptyList()));
  245 +}
  246 +```
  247 +
  248 +- [ ] **Step 1: 写失败测试**
  249 + - `UserControllerTest.java` 完整(`@WebMvcTest(UserController.class)` + `@Import(SecurityConfig, JwtAuthenticationFilter, BeanConfig, JwtUtil, JwtProperties)` + `@MockBean UserService`):
  250 + - `createUser_noAuth_returns401` — 无 auth header → Spring Security 返回 401
  251 + - `createUser_validRequest_returns200` — `superAdmin()` + mock service返回VO → 验证 code=200, data.userId 非空
  252 + - `createUser_missingUserCode_returns40001` — 请求体 userCode 为空 → 验证 code=40001
  253 + - `getStaffs_returns200` — `superAdmin()` + mock returns list → 验证 code=200
  254 + - `getPermissionGroups_returns200` — `superAdmin()` + mock returns list → 验证 code=200
  255 + - 子会话确认 FAIL(UserController 不存在)
  256 +
  257 +- [ ] **Step 2: 实现 UserController**
  258 + - 按 API shape 创建 `UserController.java`
  259 + - `createUser`: `Result.ok(userService.createUser(req, principal))`
  260 + - `getStaffs`: `Result.ok(userService.getStaffs(principal.brandId()))`
  261 + - `getPermissionGroups`: `Result.ok(userService.getPermissionGroups(principal.brandId()))`
  262 +
  263 +- [ ] **Step 3: 子会话验证 UserControllerTest 全部通过**
  264 + - 命令:`JAVA_HOME=... mvn test -pl backend -Dtest=UserControllerTest,UserServiceTest,AuthControllerTest,AuthServiceTest`
  265 +
  266 +- [ ] **Step 4: Commit**
  267 + - `git add backend/src/`
  268 + - `git commit -m "feat(usr): UserController POST/users GET/staffs GET/permission-groups REQ-USR-001"`
  269 +
  270 +---
  271 +
  272 +### Task 5: 前端 api/usr.ts + UserFormDrawer + UserListPage + App.tsx 路由
  273 +
  274 +**Files:**
  275 +- Create: `frontend/src/api/usr.ts`
  276 +- Create: `frontend/src/pages/usr/UserListPage.tsx`
  277 +- Create: `frontend/src/pages/usr/UserFormDrawer.tsx`
  278 +- Create: `frontend/src/test/UserListPage.test.tsx`
  279 +- Modify: `frontend/src/App.tsx`
  280 +
  281 +**api/usr.ts 接口**:
  282 +```ts
  283 +export interface StaffVO { sId: string; sStaffName: string }
  284 +export interface PermissionGroupVO { sId: string; sGroupCode: string; sGroupName: string; sCategory: string | null }
  285 +export interface UserCreateReq {
  286 + userCode: string; username: string; userType: '普通用户' | '超级管理员';
  287 + language: '中文' | '英文' | '繁体'; canEditDoc?: boolean;
  288 + employeeId?: string | null; permGroupIds?: string[]
  289 +}
  290 +export interface UserCreateResp { userId: string; userCode: string; username: string }
  291 +
  292 +export function getStaffs(): Promise<StaffVO[]>
  293 +export function getPermissionGroups(): Promise<PermissionGroupVO[]>
  294 +export function createUser(req: UserCreateReq): Promise<UserCreateResp>
  295 +```
  296 +(`request.get('/usr/users/staffs')` 等)
  297 +
  298 +**UserFormDrawer.tsx** props: `open: boolean`, `onClose: () => void`, `onSuccess: () => void`
  299 +- `useEffect` 拉取 staffs + permissionGroups
  300 +- Form 字段:userCode(Input), username(Input), userType(Select), language(Select), canEditDoc(Checkbox), employeeId(Select, options=staffs), permissions(Table with checkbox column)
  301 +- 提交:调 `createUser()`,成功 → `message.success('新增用户成功')` + `onSuccess()`;失败 → `message.error(e.message)`
  302 +
  303 +**UserListPage.tsx**:
  304 +- 顶部「新增」按钮(`<PermButton permission="usr:create">`,由 authSlice 中 userType 控制显示)
  305 +- 点击按钮 → `setDrawerOpen(true)`
  306 +- `<UserFormDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} onSuccess={() => setDrawerOpen(false)} />`
  307 +- 表格区域为 stub(empty Table,REQ-USR-003 补充)
  308 +
  309 +**PermButton**(如不存在则在本任务创建 `frontend/src/components/PermButton.tsx`):
  310 +```tsx
  311 +// REQ-USR-001: 权限按钮,根据 userType 控制显示
  312 +export function PermButton({ permission, children, ...props }: { permission: string } & ButtonProps) {
  313 + const userType = useAppSelector(s => s.auth.userInfo?.userType)
  314 + // usr:create / usr:edit 仅超级管理员可见
  315 + if (userType !== '超级管理员') return null
  316 + return <Button {...props}>{children}</Button>
  317 +}
  318 +```
  319 +
  320 +**App.tsx 新增路由**:
  321 +```tsx
  322 +<Route path="/usr/users" element={<PrivateRoute><UserListPage /></PrivateRoute>} />
  323 +```
  324 +
  325 +**测试 `UserListPage.test.tsx`**(三个测试):
  326 +1. `superAdmin_seesNewButton` — store userType=超级管理员 → 「新增」按钮可见
  327 +2. `normalUser_doesNotSeeNewButton` — store userType=普通用户 → 「新增」按钮不可见
  328 +3. `clickNewButton_opensDrawer` — 超级管理员点击新增 → UserFormDrawer 出现(mock API calls with vi.mock)
  329 +
  330 +- [ ] **Step 1: 写失败测试**
  331 + - 写 `UserListPage.test.tsx` 三个测试(引用 `UserListPage`、`PermButton`)
  332 + - 子会话确认 FAIL(文件不存在)
  333 +
  334 +- [ ] **Step 2: 实现**
  335 + - 按上方规格创建 `api/usr.ts`、`components/PermButton.tsx`、`UserFormDrawer.tsx`、`UserListPage.tsx`
  336 + - 修改 `App.tsx` 添加路由
  337 +
  338 +- [ ] **Step 3: 子会话验证前端测试通过**
  339 + - 命令:`cd frontend && npm run test -- --run`
  340 +
  341 +- [ ] **Step 4: Commit**
  342 + - `git add frontend/src/`
  343 + - `git commit -m "feat(usr): 前端 UserListPage + UserFormDrawer + usr.ts + PermButton REQ-USR-001"`
... ...
docs/superpowers/plans/2026-05-08-REQ-USR-002.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-002
  3 +date: 2026-05-08
  4 +spec_ref: docs/superpowers/specs/2026-05-08-REQ-USR-002.md
  5 +---
  6 +
  7 +# 修改用户 Implementation Plan
  8 +
  9 +> **Execution:** Parent skill `feature-tdd` executes this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
  10 +
  11 +**Goal:** 实现 `PUT /api/usr/users/{userId}`,允许超级管理员修改用户类型、语言、权限等,并在前端用户列表增加"修改"按钮触发编辑抽屉。
  12 +
  13 +**Architecture:** 后端遵循现有三层(Controller → Service → Mapper)模式,新增 UserUpdateReqDTO/UserUpdateRespVO,UserServiceImpl.updateUser 做权限、存在性、自身角色三重校验后执行字段更新 + 权限组先删后插;前端复用 UserFormDrawer(新增 userId/initialData props 区分 create/edit 模式),UserListPage 增加"操作"列。
  14 +
  15 +**Tech Stack:** Spring Boot 3 / MyBatis-Plus LambdaUpdateWrapper / @Transactional / React 18 / Ant Design 5 / Vitest
  16 +
  17 +---
  18 +
  19 +## Schema 改动
  20 +
  21 +无(不需要新 migration)
  22 +
  23 +## 文件变更清单
  24 +
  25 +- Create: `backend/src/main/java/com/example/erp/module/usr/dto/UserUpdateReqDTO.java` — 修改用户请求体
  26 +- Create: `backend/src/main/java/com/example/erp/module/usr/vo/UserUpdateRespVO.java` — 修改用户响应 VO
  27 +- Modify: `backend/src/main/java/com/example/erp/common/constants/UsrErrorCode.java` — 追加 USER_NOT_FOUND=40400 / SELF_ADMIN_CHANGE=40301
  28 +- Modify: `backend/src/main/java/com/example/erp/module/usr/service/UserService.java` — 追加 updateUser 接口方法
  29 +- Modify: `backend/src/main/java/com/example/erp/module/usr/service/impl/UserServiceImpl.java` — 实现 updateUser
  30 +- Modify: `backend/src/main/java/com/example/erp/module/usr/controller/UserController.java` — 追加 PUT /users/{userId}
  31 +- Modify: `backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java` — 追加 updateUser 测试
  32 +- Modify: `backend/src/test/java/com/example/erp/module/usr/UserControllerTest.java` — 追加 HTTP 测试
  33 +- Modify: `frontend/src/api/usr.ts` — 追加 UserUpdateReq / UserUpdateResp / updateUser()
  34 +- Modify: `frontend/src/pages/usr/UserFormDrawer.tsx` — 支持 edit 模式(userId + initialData props)
  35 +- Modify: `frontend/src/pages/usr/UserListPage.tsx` — 追加操作列 + editingUser 状态
  36 +- Modify: `frontend/src/test/UserListPage.test.tsx` — 追加编辑流程测试
  37 +
  38 +---
  39 +
  40 +## 任务步骤
  41 +
  42 +### Task 1: 错误码 + DTO/VO 数据结构
  43 +
  44 +**Files:**
  45 +- Modify: `backend/src/main/java/com/example/erp/common/constants/UsrErrorCode.java`
  46 +- Create: `backend/src/main/java/com/example/erp/module/usr/dto/UserUpdateReqDTO.java`
  47 +- Create: `backend/src/main/java/com/example/erp/module/usr/vo/UserUpdateRespVO.java`
  48 +- Test: `backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java`
  49 +
  50 +**API shape:**
  51 +```
  52 +UserUpdateReqDTO {
  53 + String userType; // "普通用户" | "超级管理员"
  54 + String language; // "中文" | "英文" | "繁体"
  55 + boolean canEditDoc;
  56 + boolean isDisabled;
  57 + String employeeId; // nullable
  58 + List<String> permGroupIds;
  59 +}
  60 +
  61 +UserUpdateRespVO {
  62 + String userId;
  63 + String username;
  64 + LocalDateTime updatedAt;
  65 +}
  66 +
  67 +UsrErrorCode.USER_NOT_FOUND = 40400
  68 +UsrErrorCode.SELF_ADMIN_CHANGE = 40301
  69 +```
  70 +
  71 +- [ ] **Step 1: 写失败测试**
  72 + - 测试名: `UserServiceTest#updateUser_dtoDefaults`
  73 + - 意图: `new UserUpdateReqDTO()` 后各字段可 set/get;`new UserUpdateRespVO()` 可 set/get userId/username/updatedAt;`UsrErrorCode.USER_NOT_FOUND == 40400`,`UsrErrorCode.SELF_ADMIN_CHANGE == 40301`
  74 + - 子会话确认 FAIL(类不存在)
  75 +
  76 +- [ ] **Step 2: 实现最小代码**
  77 + - 在 UsrErrorCode 追加两个常量
  78 + - 创建 UserUpdateReqDTO(@Getter @Setter,6 个字段)
  79 + - 创建 UserUpdateRespVO(@Getter @Setter,3 个字段:userId/username/updatedAt: LocalDateTime)
  80 +
  81 +- [ ] **Step 3: 子会话验证 PASS**
  82 +
  83 +- [ ] **Step 4: Commit**
  84 + - `git add backend/src/main/java/com/example/erp/common/constants/UsrErrorCode.java backend/src/main/java/com/example/erp/module/usr/dto/UserUpdateReqDTO.java backend/src/main/java/com/example/erp/module/usr/vo/UserUpdateRespVO.java backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java`
  85 + - `git commit -m "feat(usr): 添加 UserUpdateReqDTO / UserUpdateRespVO / 错误码 REQ-USR-002"`
  86 +
  87 +---
  88 +
  89 +### Task 2: UserService.updateUser 实现(含权限校验与权限组 replace)
  90 +
  91 +**Files:**
  92 +- Modify: `backend/src/main/java/com/example/erp/module/usr/service/UserService.java`
  93 +- Modify: `backend/src/main/java/com/example/erp/module/usr/service/impl/UserServiceImpl.java`
  94 +- Test: `backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java`
  95 +
  96 +**API shape:**
  97 +```
  98 +UserService#updateUser(String userId, UserUpdateReqDTO req, UserPrincipal principal) : UserUpdateRespVO
  99 +```
  100 +
  101 +业务逻辑约束(按顺序执行):
  102 +1. `!"超级管理员".equals(principal.userType())` → `BizException(PERMISSION_DENIED, "权限不足")`
  103 +2. `userMapper.selectOne(sId=userId AND sBrandsId=brandId)` == null → `BizException(USER_NOT_FOUND, "用户不存在")`
  104 +3. `principal.userId().equals(userId) && !"超级管理员".equals(req.getUserType())` → `BizException(SELF_ADMIN_CHANGE, "禁止修改自己的管理员角色")`
  105 +4. `req.getEmployeeId() != null` → staffMapper 校验存在性,不存在 → `BizException(EMPLOYEE_NOT_FOUND, "员工不存在")`
  106 +5. 用 `LambdaUpdateWrapper<UsrUserEntity>` 更新 sUserType/sLanguage/bCanEditDoc/bIsDisabled/sEmployeeId(按 sId=userId AND sBrandsId=brandId)
  107 +6. `userPermissionMapper.delete(sUserId=userId AND sBrandsId=brandId)`,再批量 insert 新 UserPermissionEntity 列表(permGroupIds 为空时只删不插)
  108 +7. 返回 UserUpdateRespVO(userId, username=entity.sUsername, updatedAt=LocalDateTime.now())
  109 +
  110 +- [ ] **Step 1: 写失败测试(共 6 个)**
  111 +
  112 + **T1** `updateUser_nonAdmin_throws40300`
  113 + - 意图: principal.userType="普通用户" → BizException.code==40300
  114 + - principal: new UserPrincipal("u1","admin","普通用户","b1")
  115 +
  116 + **T2** `updateUser_userNotFound_throws40400`
  117 + - 意图: 超级管理员 principal,userMapper.selectOne 返回 null → BizException.code==40400
  118 +
  119 + **T3** `updateUser_selfAdminChange_throws40301`
  120 + - 意图: principal.userId=="u1",userId=="u1",req.userType="普通用户" → BizException.code==40301
  121 + - userMapper.selectOne 需 stub 返回一个存在的用户实体
  122 +
  123 + **T4** `updateUser_invalidEmployee_throws40001`
  124 + - 意图: req.employeeId="bad-emp",staffMapper.selectOne 返回 null → BizException.code==40001
  125 + - userMapper.selectOne stub 返回有效用户;principal.userId!="u-target" 确保不触发 40301
  126 +
  127 + **T5** `updateUser_happyPath_updatesAndReturnsResp`
  128 + - 意图: 一切校验通过 → userMapper 执行 update;permissionMapper 先 delete 再 insert;返回 vo.userId/username/updatedAt 非 null
  129 + - Stub: userMapper.selectOne 返回含 sUsername="alice" 的实体;staffMapper.selectOne 返回非 null(若 employeeId 非 null);userMapper.update 返回 1;delete/insert 正常
  130 +
  131 + **T6** `updateUser_emptyPermGroupIds_onlyDeletes`
  132 + - 意图: req.permGroupIds=[] → userPermissionMapper.delete 被调用;insert 不被调用
  133 + - verify(userPermissionMapper, never()).insert(anyList())
  134 +
  135 + - 子会话确认 6 个测试均 FAIL(方法不存在)
  136 +
  137 +- [ ] **Step 2: 在 UserService 接口追加方法声明,在 UserServiceImpl 实现 updateUser**
  138 + - 使用 @Transactional
  139 + - 注意: UsrUserEntity 已有 sId / sUserType / sLanguage / bCanEditDoc / bIsDisabled / sEmployeeId 字段(来自现有代码)
  140 +
  141 +- [ ] **Step 3: 子会话验证全部 PASS**
  142 +
  143 +- [ ] **Step 4: Commit**
  144 + - `git add backend/src/main/java/com/example/erp/module/usr/service/ backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java`
  145 + - `git commit -m "feat(usr): UserService.updateUser 三重校验 + 权限组 replace REQ-USR-002"`
  146 +
  147 +---
  148 +
  149 +### Task 3: UserController PUT /api/usr/users/{userId}
  150 +
  151 +**Files:**
  152 +- Modify: `backend/src/main/java/com/example/erp/module/usr/controller/UserController.java`
  153 +- Test: `backend/src/test/java/com/example/erp/module/usr/UserControllerTest.java`
  154 +
  155 +**API shape:**
  156 +```
  157 +@PutMapping("/users/{userId}")
  158 +public Result<UserUpdateRespVO> updateUser(
  159 + @PathVariable String userId,
  160 + @Valid @RequestBody UserUpdateReqDTO req,
  161 + @AuthenticationPrincipal UserPrincipal principal)
  162 +// REQ-USR-002: 修改用户
  163 +```
  164 +
  165 +- [ ] **Step 1: 写失败测试(共 2 个)**
  166 +
  167 + **T1** `updateUser_withToken_returns200`
  168 + - 意图: PUT /api/usr/users/u-target,含超级管理员 Token,body={userType,language,canEditDoc,isDisabled,permGroupIds}
  169 + - stub: userService.updateUser(any,any,any) 返回 UserUpdateRespVO{userId="u-target",username="alice",updatedAt=now}
  170 + - 断言: HTTP 200, $.code==200, $.data.userId=="u-target", $.data.username=="alice"
  171 +
  172 + **T2** `updateUser_noAuth_returns401`
  173 + - 意图: 无 Token → HTTP 401
  174 +
  175 + - 子会话确认 FAIL(endpoint 不存在)
  176 +
  177 +- [ ] **Step 2: 在 UserController 追加 PUT /users/{userId} 方法,加 // REQ-USR-002: 修改用户 注释**
  178 +
  179 +- [ ] **Step 3: 子会话验证 PASS**
  180 +
  181 +- [ ] **Step 4: Commit**
  182 + - `git add backend/src/main/java/com/example/erp/module/usr/controller/UserController.java backend/src/test/java/com/example/erp/module/usr/UserControllerTest.java`
  183 + - `git commit -m "feat(usr): PUT /api/usr/users/{userId} REQ-USR-002"`
  184 +
  185 +---
  186 +
  187 +### Task 4: 前端 API + UserFormDrawer edit 模式
  188 +
  189 +**Files:**
  190 +- Modify: `frontend/src/api/usr.ts`
  191 +- Modify: `frontend/src/pages/usr/UserFormDrawer.tsx`
  192 +- Test: `frontend/src/test/UserListPage.test.tsx`
  193 +
  194 +**API shape (usr.ts):**
  195 +```ts
  196 +export interface UserUpdateReq {
  197 + userType: string
  198 + language: string
  199 + canEditDoc: boolean
  200 + isDisabled: boolean
  201 + employeeId: string | null
  202 + permGroupIds: string[]
  203 +}
  204 +
  205 +export interface UserUpdateResp {
  206 + userId: string
  207 + username: string
  208 + updatedAt: string
  209 +}
  210 +
  211 +export function updateUser(userId: string, req: UserUpdateReq): Promise<UserUpdateResp>
  212 +// → request.put(`/usr/users/${userId}`, req)
  213 +```
  214 +
  215 +**UserFormDrawer props 扩展:**
  216 +```ts
  217 +interface Props {
  218 + open: boolean
  219 + onClose: () => void
  220 + onSuccess: () => void
  221 + userId?: string // 非 null = edit 模式
  222 + initialData?: {
  223 + userType: string
  224 + language: string
  225 + canEditDoc: boolean
  226 + isDisabled: boolean
  227 + employeeId?: string | null
  228 + }
  229 +}
  230 +```
  231 +edit 模式行为:
  232 +- 抽屉 title="修改用户",隐藏 userCode/username Form.Item
  233 +- open 时用 initialData 初始化 form(form.setFieldsValue),selectedPermIds=[]
  234 +- 提交调 updateUser(userId, req) 而非 createUser;req.isDisabled 从 form.values.isDisabled 读取(Checkbox valuePropName="checked")
  235 +
  236 +- [ ] **Step 1: 写失败测试**
  237 +
  238 + 在 `UserListPage.test.tsx` vi.mock 里追加:
  239 + ```ts
  240 + updateUser: vi.fn().mockResolvedValue({ userId: 'u1', username: 'alice', updatedAt: '2026-05-08T10:00:00' })
  241 + ```
  242 +
  243 + **T1** `editMode_drawerTitle_shows修改用户`
  244 + - renderPage,设 drawerOpen=true + editingUser 存在(通过点击模拟行上的修改按钮)
  245 + - 断言: screen 中存在 "修改用户" 文本
  246 +
  247 + **T2** `editMode_submit_callsUpdateUser`
  248 + - 打开 edit 抽屉,点击确认
  249 + - waitFor: vi.mocked(updateUser) 被调用 1 次
  250 +
  251 + - 子会话确认 FAIL(updateUser 不存在于 mock 和 usr.ts)
  252 +
  253 +- [ ] **Step 2: 在 usr.ts 追加 UserUpdateReq / UserUpdateResp / updateUser;改造 UserFormDrawer 支持 edit props**
  254 +
  255 +- [ ] **Step 3: 子会话验证 PASS**
  256 +
  257 +- [ ] **Step 4: Commit**
  258 + - `git add frontend/src/api/usr.ts frontend/src/pages/usr/UserFormDrawer.tsx frontend/src/test/UserListPage.test.tsx`
  259 + - `git commit -m "feat(usr): updateUser API + UserFormDrawer edit 模式 REQ-USR-002"`
  260 +
  261 +---
  262 +
  263 +### Task 5: UserListPage 操作列 + editingUser 状态
  264 +
  265 +**Files:**
  266 +- Modify: `frontend/src/pages/usr/UserListPage.tsx`
  267 +- Test: `frontend/src/test/UserListPage.test.tsx`
  268 +
  269 +**行为:**
  270 +- columns 末尾追加 "操作" 列,每行渲染 `<PermButton permission="usr:edit" onClick={() => setEditingUser(row)}>修改</PermButton>`
  271 +- 新增状态: `const [editingUser, setEditingUser] = useState<UserListItemVO | null>(null)`
  272 +- UserFormDrawer 新增两个 props:
  273 + - `open={drawerOpen || editingUser !== null}`(或用 editDrawerOpen 独立状态)
  274 + - `userId={editingUser?.sId}`
  275 + - `initialData={editingUser ? { userType: editingUser.sUserType, language: editingUser.sLanguage, canEditDoc: false, isDisabled: editingUser.bIsDisabled === 1, employeeId: null } : undefined}`
  276 + - 注意:UserListItemVO 不含 bCanEditDoc,edit 模式下默认 false,用户可手动勾选
  277 + - `onSuccess={() => { setEditingUser(null); load(1) }}`
  278 + - `onClose={() => setEditingUser(null)}`
  279 +
  280 + 注意:区分新增抽屉(drawerOpen)和编辑抽屉(editingUser !== null),两者可独立触发 UserFormDrawer,也可复用同一个实例——推荐独立实例避免状态冲突:新增用 drawerOpen/setDrawerOpen,编辑用 editingUser/setEditingUser。
  281 +
  282 +- [ ] **Step 1: 写失败测试**
  283 +
  284 + **T1** `editButton_openEditDrawer`
  285 + - mock getUserList 返回 1 行(sId='u1',sUsername='alice',...)
  286 + - renderPage('超级管理员')
  287 + - waitFor: screen 中有 "alice" 渲染出来
  288 + - 点击 "修改" 按钮
  289 + - waitFor: screen 中有 "修改用户" 文本(抽屉标题)
  290 +
  291 + - 子会话确认 FAIL(无"修改"按钮)
  292 +
  293 +- [ ] **Step 2: 在 UserListPage 追加操作列和编辑抽屉逻辑;追加 // REQ-USR-002: 修改用户 注释**
  294 +
  295 +- [ ] **Step 3: 子会话验证 PASS(全量测试:后端 45+ / 前端 11+)**
  296 +
  297 +- [ ] **Step 4: Commit**
  298 + - `git add frontend/src/pages/usr/UserListPage.tsx frontend/src/test/UserListPage.test.tsx`
  299 + - `git commit -m "feat(usr): UserListPage 修改按钮 + 编辑抽屉 REQ-USR-002"`
  300 +
  301 +---
  302 +
  303 +## 提交计划
  304 +
  305 +- `feat(usr): 添加 UserUpdateReqDTO / UserUpdateRespVO / 错误码 REQ-USR-002`(覆盖 Task 1)
  306 +- `feat(usr): UserService.updateUser 三重校验 + 权限组 replace REQ-USR-002`(覆盖 Task 2)
  307 +- `feat(usr): PUT /api/usr/users/{userId} REQ-USR-002`(覆盖 Task 3)
  308 +- `feat(usr): updateUser API + UserFormDrawer edit 模式 REQ-USR-002`(覆盖 Task 4)
  309 +- `feat(usr): UserListPage 修改按钮 + 编辑抽屉 REQ-USR-002`(覆盖 Task 5)
... ...
docs/superpowers/plans/2026-05-08-REQ-USR-003.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-003
  3 +date: 2026-05-08
  4 +spec_ref: docs/superpowers/specs/2026-05-08-REQ-USR-003.md
  5 +---
  6 +
  7 +# Plan: REQ-USR-003 查询用户
  8 +
  9 +> **Execution:** Parent skill `feature-tdd` executes this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
  10 +
  11 +**Goal:** 实现 `GET /api/usr/users` 动态查询分页接口(8 字段 × 3 匹配方式,LEFT JOIN tStaff)及前端搜索列表页。
  12 +
  13 +**Architecture:** 自定义 MyBatis XML Mapper 实现 LEFT JOIN 动态 SQL + MyBatis-Plus `IPage` 分页;Service 层负责 pageSize 上限截断与 queryValue 空值短路;Controller 收 `@RequestParam`,`@AuthenticationPrincipal` 取 brandId。前端用 `useEffect` 初始加载 + 搜索按钮触发重新查询,Ant Design Table 接收 `PageVO` 分页数据。
  14 +
  15 +**Tech Stack:** Spring Boot 3 / MyBatis-Plus 3.x / JUnit 5 + Mockito / React 18 / Ant Design 5 / Vitest + @testing-library/react
  16 +
  17 +---
  18 +
  19 +## Schema 改动
  20 +无(V1 已包含 usr_user、tStaff 所有字段)
  21 +
  22 +## 文件变更清单
  23 +
  24 +- Create: `backend/src/main/java/com/example/erp/common/vo/PageVO.java` — 通用分页包装(含 `static of(IPage<T>)` 工厂)
  25 +- Create: `backend/src/main/java/com/example/erp/module/usr/dto/UserListQueryDTO.java` — 查询入参 DTO
  26 +- Create: `backend/src/main/java/com/example/erp/module/usr/vo/UserListItemVO.java` — 列表项 VO(11 字段)
  27 +- Create: `backend/src/main/resources/mapper/UsrUserMapper.xml` — LEFT JOIN 动态分页 SQL
  28 +- Modify: `backend/src/main/java/com/example/erp/module/usr/mapper/UsrUserMapper.java` — 添加 `selectUserList`
  29 +- Modify: `backend/src/main/java/com/example/erp/module/usr/service/UserService.java` — 添加 `getUserList` 签名
  30 +- Modify: `backend/src/main/java/com/example/erp/module/usr/service/impl/UserServiceImpl.java` — 实现 `getUserList`
  31 +- Modify: `backend/src/main/java/com/example/erp/module/usr/controller/UserController.java` — 添加 `GET /api/usr/users`
  32 +- Modify: `backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java` — 添加 `getUserList` 测试
  33 +- Modify: `backend/src/test/java/com/example/erp/module/usr/UserControllerTest.java` — 添加 `getUsers` 测试
  34 +- Modify: `frontend/src/api/usr.ts` — 添加 `UserListQueryReq`, `UserListItemVO`, `PageVO`, `getUserList()`
  35 +- Modify: `frontend/src/pages/usr/UserListPage.tsx` — 搜索表单 + 真实 Table + 分页
  36 +- Modify: `frontend/src/test/UserListPage.test.tsx` — 添加列表初始加载测试
  37 +
  38 +## 任务步骤
  39 +
  40 +---
  41 +
  42 +### Task 1: 后端 VO/DTO 骨架(PageVO + UserListItemVO + UserListQueryDTO)
  43 +
  44 +**Files:**
  45 +- Create: `backend/src/main/java/com/example/erp/common/vo/PageVO.java`
  46 +- Create: `backend/src/main/java/com/example/erp/module/usr/dto/UserListQueryDTO.java`
  47 +- Create: `backend/src/main/java/com/example/erp/module/usr/vo/UserListItemVO.java`
  48 +- Test: `backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java`
  49 +
  50 +**API shape(锁定签名):**
  51 +```java
  52 +// PageVO<T> — com.example.erp.common.vo
  53 +@Getter @Setter
  54 +public class PageVO<T> {
  55 + private long total;
  56 + private long page;
  57 + private long pageSize;
  58 + private List<T> list;
  59 +
  60 + public static <T> PageVO<T> of(IPage<T> iPage) {
  61 + PageVO<T> vo = new PageVO<>();
  62 + vo.total = iPage.getTotal();
  63 + vo.page = iPage.getCurrent();
  64 + vo.pageSize = iPage.getSize();
  65 + vo.list = iPage.getRecords();
  66 + return vo;
  67 + }
  68 +}
  69 +
  70 +// UserListQueryDTO — com.example.erp.module.usr.dto
  71 +@Getter @Setter
  72 +public class UserListQueryDTO {
  73 + private String queryField = "username";
  74 + private String matchType = "contains";
  75 + private String queryValue;
  76 + private int page = 1;
  77 + private int pageSize = 20;
  78 +}
  79 +
  80 +// UserListItemVO — com.example.erp.module.usr.vo(全部字段加 @JsonProperty)
  81 +@Getter @Setter
  82 +public class UserListItemVO {
  83 + @JsonProperty("sId") private String sId;
  84 + @JsonProperty("sUsername") private String sUsername;
  85 + @JsonProperty("sUserCode") private String sUserCode;
  86 + @JsonProperty("sUserType") private String sUserType;
  87 + @JsonProperty("sLanguage") private String sLanguage;
  88 + @JsonProperty("bIsDisabled") private Integer bIsDisabled;
  89 + @JsonProperty("tLastLoginDate") private LocalDateTime tLastLoginDate;
  90 + @JsonProperty("sCreatorUsername") private String sCreatorUsername;
  91 + @JsonProperty("tCreateDate") private LocalDateTime tCreateDate;
  92 + @JsonProperty("sStaffName") private String sStaffName;
  93 + @JsonProperty("sDepartment") private String sDepartment;
  94 +}
  95 +```
  96 +
  97 +- [ ] **Step 1: 写失败测试**
  98 + - 在 `UserServiceTest.java` 顶部 import `UserListQueryDTO`
  99 + - 添加占位测试方法(方法体可为空,仅引用该类型使编译失败):
  100 + ```java
  101 + @Test
  102 + void getUserList_capsPageSizeAt100() {
  103 + UserListQueryDTO q = new UserListQueryDTO();
  104 + q.setPageSize(200);
  105 + }
  106 + ```
  107 + - 子会话确认:`mvn test -pl backend -Dtest=UserServiceTest` 编译失败(`UserListQueryDTO` 符号找不到)
  108 +
  109 +- [ ] **Step 2: 创建 VO/DTO**
  110 + - 创建 `PageVO.java`(含 `of()` 工厂)
  111 + - 创建 `UserListQueryDTO.java`
  112 + - 创建 `UserListItemVO.java`
  113 +
  114 +- [ ] **Step 3: 子会话验证 PASS**
  115 + - `mvn test -pl backend -Dtest=UserServiceTest` 全部通过
  116 +
  117 +- [ ] **Step 4: Commit**
  118 + - `git add backend/src/main/java/com/example/erp/common/vo/PageVO.java backend/src/main/java/com/example/erp/module/usr/dto/UserListQueryDTO.java backend/src/main/java/com/example/erp/module/usr/vo/UserListItemVO.java backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java`
  119 + - `git commit -m "feat(usr): 添加 PageVO / UserListQueryDTO / UserListItemVO REQ-USR-003"`
  120 +
  121 +---
  122 +
  123 +### Task 2: 自定义 Mapper(UsrUserMapper.xml + 方法声明)
  124 +
  125 +**Files:**
  126 +- Create: `backend/src/main/resources/mapper/UsrUserMapper.xml`
  127 +- Modify: `backend/src/main/java/com/example/erp/module/usr/mapper/UsrUserMapper.java`
  128 +- Test: `backend/src/test/java/com/example/erp/module/usr/UserServiceTest.java`
  129 +
  130 +**API shape(锁定方法签名):**
  131 +```java
  132 +// UsrUserMapper.java — 新增方法
  133 +IPage<UserListItemVO> selectUserList(
  134 + IPage<UserListItemVO> page,
  135 + @Param("brandId") String brandId,
  136 + @Param("queryField") String queryField,
  137 + @Param("matchType") String matchType,
  138 + @Param("queryValue") String queryValue
  139 +);
  140 +```
  141 +
  142 +**XML SQL 完整内容(UsrUserMapper.xml):**
  143 +```xml
  144 +<?xml version="1.0" encoding="UTF-8"?>
  145 +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  146 + "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  147 +<mapper namespace="com.example.erp.module.usr.mapper.UsrUserMapper">
  148 +
  149 + <select id="selectUserList" resultType="com.example.erp.module.usr.vo.UserListItemVO">
  150 + SELECT u.sId, u.sUsername, u.sUserCode, u.sUserType, u.sLanguage,
  151 + u.bIsDisabled, u.tLastLoginDate, u.sCreatorUsername, u.tCreateDate,
  152 + s.sStaffName, s.sDepartment
  153 + FROM usr_user u
  154 + LEFT JOIN tStaff s ON u.sEmployeeId = s.sId
  155 + AND s.sBrandsId = u.sBrandsId
  156 + AND s.bDeleted = 0
  157 + WHERE u.sBrandsId = #{brandId}
  158 + <if test="queryValue != null and queryValue != ''">
  159 + <choose>
  160 + <when test="queryField == 'username'">
  161 + <choose>
  162 + <when test="matchType == 'contains'">AND u.sUsername LIKE CONCAT('%', #{queryValue}, '%')</when>
  163 + <when test="matchType == 'notContains'">AND u.sUsername NOT LIKE CONCAT('%', #{queryValue}, '%')</when>
  164 + <otherwise>AND u.sUsername = #{queryValue}</otherwise>
  165 + </choose>
  166 + </when>
  167 + <when test="queryField == 'staffName'">
  168 + <choose>
  169 + <when test="matchType == 'contains'">AND s.sStaffName LIKE CONCAT('%', #{queryValue}, '%')</when>
  170 + <when test="matchType == 'notContains'">AND s.sStaffName NOT LIKE CONCAT('%', #{queryValue}, '%')</when>
  171 + <otherwise>AND s.sStaffName = #{queryValue}</otherwise>
  172 + </choose>
  173 + </when>
  174 + <when test="queryField == 'userCode'">
  175 + <choose>
  176 + <when test="matchType == 'contains'">AND u.sUserCode LIKE CONCAT('%', #{queryValue}, '%')</when>
  177 + <when test="matchType == 'notContains'">AND u.sUserCode NOT LIKE CONCAT('%', #{queryValue}, '%')</when>
  178 + <otherwise>AND u.sUserCode = #{queryValue}</otherwise>
  179 + </choose>
  180 + </when>
  181 + <when test="queryField == 'department'">
  182 + <choose>
  183 + <when test="matchType == 'contains'">AND s.sDepartment LIKE CONCAT('%', #{queryValue}, '%')</when>
  184 + <when test="matchType == 'notContains'">AND s.sDepartment NOT LIKE CONCAT('%', #{queryValue}, '%')</when>
  185 + <otherwise>AND s.sDepartment = #{queryValue}</otherwise>
  186 + </choose>
  187 + </when>
  188 + <when test="queryField == 'userType'">
  189 + <choose>
  190 + <when test="matchType == 'contains'">AND u.sUserType LIKE CONCAT('%', #{queryValue}, '%')</when>
  191 + <when test="matchType == 'notContains'">AND u.sUserType NOT LIKE CONCAT('%', #{queryValue}, '%')</when>
  192 + <otherwise>AND u.sUserType = #{queryValue}</otherwise>
  193 + </choose>
  194 + </when>
  195 + <when test="queryField == 'creator'">
  196 + <choose>
  197 + <when test="matchType == 'contains'">AND u.sCreatorUsername LIKE CONCAT('%', #{queryValue}, '%')</when>
  198 + <when test="matchType == 'notContains'">AND u.sCreatorUsername NOT LIKE CONCAT('%', #{queryValue}, '%')</when>
  199 + <otherwise>AND u.sCreatorUsername = #{queryValue}</otherwise>
  200 + </choose>
  201 + </when>
  202 + <when test="queryField == 'disabled'">
  203 + <choose>
  204 + <when test="queryValue == '是'">AND u.bIsDisabled = 1</when>
  205 + <when test="queryValue == '否'">AND u.bIsDisabled = 0</when>
  206 + </choose>
  207 + </when>
  208 + <when test="queryField == 'lastLoginDate'">
  209 + AND DATE(u.tLastLoginDate) = #{queryValue}
  210 + </when>
  211 + </choose>
  212 + </if>
  213 + ORDER BY u.tCreateDate DESC
  214 + </select>
  215 +
  216 +</mapper>
  217 +```
  218 +
  219 +注:`mybatis-plus.mapper-locations` 默认为 `classpath*:/mapper/**/*.xml`,文件放 `src/main/resources/mapper/UsrUserMapper.xml` 会自动扫描到。
  220 +
  221 +- [ ] **Step 1: 写失败测试**
  222 + - 在 `UserServiceTest.java` 中添加 mock stub 引用 `selectUserList`:
  223 + ```java
  224 + @Test
  225 + void getUserList_callsSelectUserList() {
  226 + IPage<UserListItemVO> mockPage = new Page<>(1, 20, 0);
  227 + when(userMapper.selectUserList(any(), eq("b1"), any(), any(), any()))
  228 + .thenReturn(mockPage);
  229 + // getUserList 方法此时不存在于 UserService → 编译失败
  230 + }
  231 + ```
  232 + - 同时 `@Mock private UsrUserMapper userMapper` 已有,直接 `userMapper.selectUserList(...)` 引用即可触发编译失败
  233 + - 子会话确认 FAIL(`selectUserList` 方法不存在)
  234 +
  235 +- [ ] **Step 2: 实现 Mapper**
  236 + - `UsrUserMapper.java` 添加 `selectUserList` 方法(带 `@Param` 注解,需 import `com.baomidou.mybatisplus.extension.plugins.pagination.Page`)
  237 + - 创建 `src/main/resources/mapper/UsrUserMapper.xml`
  238 +
  239 +- [ ] **Step 3: 子会话验证 PASS**
  240 + - `mvn test -pl backend -Dtest=UserServiceTest` 通过
  241 +
  242 +- [ ] **Step 4: Commit**
  243 + - `git commit -m "feat(usr): 添加 selectUserList XML 动态分页查询 REQ-USR-003"`
  244 +
  245 +---
  246 +
  247 +### Task 3: Service 层 + Controller 端点
  248 +
  249 +**Files:**
  250 +- Modify: `backend/src/main/java/com/example/erp/module/usr/service/UserService.java`
  251 +- Modify: `backend/src/main/java/com/example/erp/module/usr/service/impl/UserServiceImpl.java`
  252 +- Modify: `backend/src/main/java/com/example/erp/module/usr/controller/UserController.java`
  253 +- Test: `UserServiceTest.java`(service 层)、`UserControllerTest.java`(HTTP 层)
  254 +
  255 +**API shape(锁定签名):**
  256 +```java
  257 +// UserService 接口
  258 +PageVO<UserListItemVO> getUserList(UserListQueryDTO query, String brandId);
  259 +
  260 +// UserServiceImpl — getUserList 实现逻辑
  261 +@Transactional(readOnly = true)
  262 +public PageVO<UserListItemVO> getUserList(UserListQueryDTO query, String brandId) {
  263 + int cappedSize = Math.min(query.getPageSize(), 100);
  264 + IPage<UserListItemVO> iPage = new Page<>(query.getPage(), cappedSize);
  265 + userMapper.selectUserList(iPage, brandId,
  266 + query.getQueryField(), query.getMatchType(),
  267 + query.getQueryValue() == null ? "" : query.getQueryValue());
  268 + return PageVO.of(iPage);
  269 +}
  270 +
  271 +// UserController — 新增端点
  272 +@GetMapping("/users")
  273 +public Result<PageVO<UserListItemVO>> getUsers(
  274 + @RequestParam(defaultValue = "username") String queryField,
  275 + @RequestParam(defaultValue = "contains") String matchType,
  276 + @RequestParam(defaultValue = "") String queryValue,
  277 + @RequestParam(defaultValue = "1") int page,
  278 + @RequestParam(defaultValue = "20") int pageSize,
  279 + @AuthenticationPrincipal UserPrincipal principal) {
  280 + UserListQueryDTO q = new UserListQueryDTO();
  281 + q.setQueryField(queryField);
  282 + q.setMatchType(matchType);
  283 + q.setQueryValue(queryValue);
  284 + q.setPage(page);
  285 + q.setPageSize(pageSize);
  286 + return Result.ok(userService.getUserList(q, principal.brandId()));
  287 +}
  288 +```
  289 +
  290 +- [ ] **Step 1: 写失败测试(Service)**
  291 + - 测试名: `UserServiceTest#getUserList_capsPageSizeAt100_callsMapper`
  292 + - 意图:`pageSize=200` → mapper 被调用时 `iPage.getSize() == 100`;返回的 `PageVO.getTotal() == 5`
  293 + - 断言 sketch:
  294 + ```java
  295 + UserListQueryDTO q = new UserListQueryDTO();
  296 + q.setPageSize(200);
  297 + IPage<UserListItemVO> mockPage = new Page<>(1, 100, 5);
  298 + when(userMapper.selectUserList(argThat(p -> p.getSize() == 100), eq("b1"), any(), any(), any()))
  299 + .thenReturn(mockPage);
  300 + PageVO<UserListItemVO> result = userService.getUserList(q, "b1"); // 编译失败
  301 + assertEquals(5, result.getTotal());
  302 + ```
  303 + - 子会话确认 FAIL(`getUserList` 不在接口/实现中)
  304 +
  305 +- [ ] **Step 2: 实现 Service**
  306 + - `UserService.java` 添加 `getUserList` 方法签名
  307 + - `UserServiceImpl.java` 实现(pageSize cap + delegate + `PageVO.of`)
  308 +
  309 +- [ ] **Step 3: 子会话验证 Service PASS**
  310 + - `mvn test -pl backend -Dtest=UserServiceTest` 通过
  311 +
  312 +- [ ] **Step 4: 写失败测试(Controller)**
  313 + - 测试名: `UserControllerTest#getUsers_withToken_returns200`
  314 + - 意图:带 JWT Token 的 `GET /api/usr/users` → HTTP 200,响应 JSON 中 `$.data.total` 存在
  315 + - mock stub:`when(userService.getUserList(any(), eq("b1"))).thenReturn(pageVO)`(pageVO.total=0, pageVO.list=[])
  316 + - 子会话确认 FAIL(端点不存在 → Spring 返回 404 或方法签名缺失导致编译失败)
  317 +
  318 +- [ ] **Step 5: 实现 Controller 端点**
  319 + - `UserController.java` 添加 `@GetMapping("/users")` 方法
  320 +
  321 +- [ ] **Step 6: 子会话验证 Controller PASS**
  322 + - `mvn test -pl backend -Dtest=UserControllerTest` 通过
  323 +
  324 +- [ ] **Step 7: Commit**
  325 + - `git commit -m "feat(usr): getUserList service + GET /api/usr/users 端点 REQ-USR-003"`
  326 +
  327 +---
  328 +
  329 +### Task 4: 前端 API 类型 + 用户列表页
  330 +
  331 +**Files:**
  332 +- Modify: `frontend/src/api/usr.ts`
  333 +- Modify: `frontend/src/pages/usr/UserListPage.tsx`
  334 +- Modify: `frontend/src/test/UserListPage.test.tsx`
  335 +
  336 +**API shape(锁定 usr.ts 新增部分):**
  337 +```ts
  338 +export interface UserListQueryReq {
  339 + queryField?: string;
  340 + matchType?: string;
  341 + queryValue?: string;
  342 + page?: number;
  343 + pageSize?: number;
  344 +}
  345 +
  346 +export interface UserListItemVO {
  347 + sId: string;
  348 + sUsername: string;
  349 + sUserCode: string;
  350 + sUserType: string;
  351 + sLanguage: string;
  352 + bIsDisabled: number;
  353 + tLastLoginDate: string | null;
  354 + sCreatorUsername: string | null;
  355 + tCreateDate: string;
  356 + sStaffName: string | null;
  357 + sDepartment: string | null;
  358 +}
  359 +
  360 +export interface PageVO<T> {
  361 + total: number;
  362 + page: number;
  363 + pageSize: number;
  364 + list: T[];
  365 +}
  366 +
  367 +export function getUserList(params?: UserListQueryReq): Promise<PageVO<UserListItemVO>> {
  368 + return request.get('/usr/users', { params })
  369 +}
  370 +```
  371 +
  372 +**UserListPage.tsx 结构(含搜索表单):**
  373 +```tsx
  374 +export default function UserListPage() {
  375 + const [data, setData] = useState<PageVO<UserListItemVO> | null>(null)
  376 + const [queryField, setQueryField] = useState('username')
  377 + const [matchType, setMatchType] = useState('contains')
  378 + const [queryValue, setQueryValue] = useState('')
  379 + const [page, setPage] = useState(1)
  380 +
  381 + const load = (pg = 1) => {
  382 + getUserList({ queryField, matchType, queryValue, page: pg, pageSize: 20 }).then(setData)
  383 + setPage(pg)
  384 + }
  385 +
  386 + useEffect(() => { load() }, []) // 初始加载
  387 +
  388 + const columns: ColumnsType<UserListItemVO> = [
  389 + { title: '用户名', dataIndex: 'sUsername' },
  390 + { title: '员工名', dataIndex: 'sStaffName' },
  391 + { title: '用户号', dataIndex: 'sUserCode' },
  392 + { title: '部门', dataIndex: 'sDepartment' },
  393 + { title: '用户类型', dataIndex: 'sUserType' },
  394 + { title: '语言', dataIndex: 'sLanguage' },
  395 + { title: '作废', dataIndex: 'bIsDisabled', render: v => v ? '是' : '否' },
  396 + { title: '登录日期', dataIndex: 'tLastLoginDate' },
  397 + { title: '制单人', dataIndex: 'sCreatorUsername' },
  398 + { title: '制单日期', dataIndex: 'tCreateDate' },
  399 + ]
  400 +
  401 + return (
  402 + <div>
  403 + <Space style={{ marginBottom: 16 }}>
  404 + <Select value={queryField} onChange={setQueryField} style={{ width: 120 }}>
  405 + <Select.Option value="username">用户名</Select.Option>
  406 + <Select.Option value="staffName">员工名</Select.Option>
  407 + <Select.Option value="userCode">用户号</Select.Option>
  408 + <Select.Option value="department">部门</Select.Option>
  409 + <Select.Option value="userType">用户类型</Select.Option>
  410 + <Select.Option value="disabled">作废</Select.Option>
  411 + <Select.Option value="lastLoginDate">登录日期</Select.Option>
  412 + <Select.Option value="creator">制单人</Select.Option>
  413 + </Select>
  414 + <Select value={matchType} onChange={setMatchType} style={{ width: 100 }}>
  415 + <Select.Option value="contains">包含</Select.Option>
  416 + <Select.Option value="notContains">不包含</Select.Option>
  417 + <Select.Option value="equals">等于</Select.Option>
  418 + </Select>
  419 + <Input value={queryValue} onChange={e => setQueryValue(e.target.value)} placeholder="查询值" />
  420 + <Button type="primary" onClick={() => load(1)}>搜索</Button>
  421 + <PermButton permission="usr:create" type="primary" onClick={() => setDrawerOpen(true)}>新增</PermButton>
  422 + </Space>
  423 + <Table
  424 + dataSource={data?.list ?? []}
  425 + columns={columns}
  426 + rowKey="sId"
  427 + pagination={{
  428 + total: data?.total ?? 0,
  429 + pageSize: 20,
  430 + current: page,
  431 + onChange: load,
  432 + }}
  433 + />
  434 + <UserFormDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} onSuccess={() => { setDrawerOpen(false); load(1) }} />
  435 + </div>
  436 + )
  437 +}
  438 +```
  439 +
  440 +注:`drawerOpen` state 也需添加(与 Task 1 中 UserListPage 原来的 `drawerOpen` 合并)。
  441 +
  442 +- [ ] **Step 1: 写失败测试**
  443 + - 测试名: `UserListPage.test.tsx#initialLoad_rendersTableRows`
  444 + - 意图:mock `getUserList` 返回含 1 条 `{ sId:'u1', sUsername:'alice', ... }` 的 PageVO → 等待 data 渲染后 table 中有 "alice" 文本
  445 + - 断言 sketch:
  446 + ```ts
  447 + vi.mock('@/api/usr', async (importOriginal) => {
  448 + const actual = await importOriginal<typeof import('@/api/usr')>()
  449 + return { ...actual, getUserList: vi.fn().mockResolvedValue({ total: 1, page: 1, pageSize: 20, list: [{ sId:'u1', sUsername:'alice', sUserCode:'UC001', sUserType:'普通用户', sLanguage:'中文', bIsDisabled:0, tLastLoginDate:null, sCreatorUsername:'admin', tCreateDate:'2026-01-01', sStaffName:null, sDepartment:null }] }) }
  450 + })
  451 + // render + waitFor → screen.getByText('alice')
  452 + ```
  453 + - 子会话确认 FAIL(`getUserList` 不在 `usr.ts` 中 → import 报错)
  454 +
  455 +- [ ] **Step 2: 实现前端**
  456 + - `usr.ts` 添加 `UserListQueryReq`, `UserListItemVO`, `PageVO`, `getUserList()`
  457 + - `UserListPage.tsx` 重构为搜索表单 + 真实 Table(保留 `drawerOpen` 和 `UserFormDrawer`)
  458 +
  459 +- [ ] **Step 3: 子会话验证 PASS**
  460 + - `pnpm test --run` 全部通过
  461 +
  462 +- [ ] **Step 4: Commit**
  463 + - `git commit -m "feat(usr): 用户列表搜索表单 + 分页表格 REQ-USR-003"`
  464 +
  465 +---
  466 +
  467 +## 提交计划
  468 +
  469 +- `feat(usr): 添加 PageVO / UserListQueryDTO / UserListItemVO REQ-USR-003`(覆盖 Task 1)
  470 +- `feat(usr): 添加 selectUserList XML 动态分页查询 REQ-USR-003`(覆盖 Task 2)
  471 +- `feat(usr): getUserList service + GET /api/usr/users 端点 REQ-USR-003`(覆盖 Task 3)
  472 +- `feat(usr): 用户列表搜索表单 + 分页表格 REQ-USR-003`(覆盖 Task 4)
... ...
docs/superpowers/plans/2026-05-08-REQ-USR-004.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-004
  3 +date: 2026-05-08
  4 +spec_ref: docs/superpowers/specs/2026-05-08-REQ-USR-004.md
  5 +---
  6 +
  7 +# REQ-USR-004 用户登录 Implementation Plan
  8 +
  9 +> **Execution:** Parent skill `feature-tdd` executes this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
  10 +
  11 +**Goal:** 从零搭建后端 Spring Boot 3 项目与前端 Vite+React 项目,实现三条公开认证接口(POST /api/auth/login、POST /api/auth/refresh、GET /api/auth/brands),完成多租户隔离的 JWT 登录认证及账号锁定防暴力破解。
  12 +
  13 +**Architecture:** 后端分四层推进:公共层(Result/BizException/JwtUtil)→ 数据访问层(BrandMapper/UsrUserMapper)→ 业务层(AuthServiceImpl,含 brand 多租户查找、BCrypt 校验、禁用/锁定检查、失败计数、JWT 签发 6 条业务规则)→ 接口层(SecurityConfig + AuthController)。前端分三层推进:项目骨架 → Axios 封装 + Redux authSlice → LoginPage.tsx 含 brand 下拉 + 用户名密码表单。
  14 +
  15 +**Tech Stack:** Spring Boot 3.x · Spring Security · JJWT 0.12.x · MyBatis-Plus 3.5.x · Flyway 10.x · MySQL 8.x · Vite · React 18 · Ant Design 5.x · Redux Toolkit · Axios · Vitest + @testing-library/react
  16 +
  17 +---
  18 +
  19 +## Schema 改动
  20 +
  21 +无(`sql/migrations/V1__initial_schema.sql` 已包含 `usr_user` + `brand` 表,已 apply 至测试库 `xlyweberp_vibe_erp_test`)
  22 +
  23 +## 文件变更清单
  24 +
  25 +### 后端(全部新建)
  26 +- `backend/pom.xml` — 创建(Spring Boot 3 Maven 项目根 POM)
  27 +- `backend/src/main/java/com/example/erp/Application.java` — 创建(启动类)
  28 +- `backend/src/main/resources/application.yml` — 创建(主配置,DB/JWT 用 `${ENV_VAR}` 占位)
  29 +- `backend/src/main/resources/application-dev.yml` — 创建(dev profile,Flyway baseline 防重复迁移)
  30 +- `backend/src/main/resources/db/migration/V1__initial_schema.sql` — 创建(内容与 `sql/migrations/V1__initial_schema.sql` 完全一致,供 Flyway classpath 找到)
  31 +- `backend/src/main/java/com/example/erp/common/response/Result.java` — 创建(统一响应体,含 timestamp)
  32 +- `backend/src/main/java/com/example/erp/common/exception/BizException.java` — 创建(业务异常基类)
  33 +- `backend/src/main/java/com/example/erp/common/constants/AuthErrorCode.java` — 创建(认证错误码常量)
  34 +- `backend/src/main/java/com/example/erp/common/exception/GlobalExceptionHandler.java` — 创建(@RestControllerAdvice)
  35 +- `backend/src/main/java/com/example/erp/common/util/JwtUtil.java` — 创建(JWT 生成 + 解析)
  36 +- `backend/src/main/java/com/example/erp/config/JwtProperties.java` — 创建(@ConfigurationProperties("jwt"))
  37 +- `backend/src/main/java/com/example/erp/module/usr/entity/BrandEntity.java` — 创建(brand 表映射)
  38 +- `backend/src/main/java/com/example/erp/module/usr/entity/UsrUserEntity.java` — 创建(usr_user 表映射)
  39 +- `backend/src/main/java/com/example/erp/module/usr/mapper/BrandMapper.java` — 创建(extends BaseMapper<BrandEntity>)
  40 +- `backend/src/main/java/com/example/erp/module/usr/mapper/UsrUserMapper.java` — 创建(extends BaseMapper<UsrUserEntity>)
  41 +- `backend/src/main/java/com/example/erp/config/MyBatisPlusConfig.java` — 创建(@MapperScan + 分页插件)
  42 +- `backend/src/main/java/com/example/erp/module/usr/dto/LoginReqDTO.java` — 创建(登录入参)
  43 +- `backend/src/main/java/com/example/erp/module/usr/dto/RefreshTokenReqDTO.java` — 创建(刷新入参)
  44 +- `backend/src/main/java/com/example/erp/module/usr/vo/LoginVO.java` — 创建(登录出参,含内部静态类 UserInfoVO)
  45 +- `backend/src/main/java/com/example/erp/module/usr/vo/BrandVO.java` — 创建(brand 下拉出参)
  46 +- `backend/src/main/java/com/example/erp/module/usr/service/AuthService.java` — 创建(接口)
  47 +- `backend/src/main/java/com/example/erp/module/usr/service/impl/AuthServiceImpl.java` — 创建(业务实现)
  48 +- `backend/src/main/java/com/example/erp/config/BeanConfig.java` — 创建(@Bean BCryptPasswordEncoder;与 SecurityConfig 分离,避免循环依赖)
  49 +- `backend/src/main/java/com/example/erp/config/SecurityConfig.java` — 创建(放行 /api/auth/**,注册 JwtFilter)
  50 +- `backend/src/main/java/com/example/erp/config/JwtAuthenticationFilter.java` — 创建(OncePerRequestFilter,验证 Bearer Token)
  51 +- `backend/src/main/java/com/example/erp/module/usr/controller/AuthController.java` — 创建(3 个端点)
  52 +- `backend/src/test/java/com/example/erp/ApplicationContextTest.java` — 创建
  53 +- `backend/src/test/java/com/example/erp/common/JwtUtilTest.java` — 创建
  54 +- `backend/src/test/java/com/example/erp/module/usr/BrandMapperTest.java` — 创建
  55 +- `backend/src/test/java/com/example/erp/module/usr/AuthServiceTest.java` — 创建
  56 +- `backend/src/test/java/com/example/erp/module/usr/AuthControllerTest.java` — 创建
  57 +
  58 +### 前端(全部新建)
  59 +- `frontend/package.json` — 创建
  60 +- `frontend/vite.config.ts` — 创建(代理 /api → http://localhost:8080)
  61 +- `frontend/tsconfig.json` — 创建
  62 +- `frontend/index.html` — 创建
  63 +- `frontend/src/main.tsx` — 创建(Provider + BrowserRouter + App)
  64 +- `frontend/src/App.tsx` — 创建(路由表,/ 重定向 /login,受保护路由守卫 PrivateRoute)
  65 +- `frontend/src/styles/tokens.css` — 创建(内容与根目录 `src/styles/tokens.css` 一致)
  66 +- `frontend/src/api/request.ts` — 创建(Axios 实例 + 请求/响应拦截器 + 401 refresh 流程)
  67 +- `frontend/src/api/auth.ts` — 创建(login / refresh / getBrands 接口函数)
  68 +- `frontend/src/store/index.ts` — 创建(configureStore)
  69 +- `frontend/src/store/slices/authSlice.ts` — 创建(setCredentials / clearCredentials)
  70 +- `frontend/src/pages/usr/LoginPage.tsx` — 创建(AntD Form:brand Select + 用户名 + 密码 + 提交)
  71 +- `frontend/src/test/setup.ts` — 创建(@testing-library/jest-dom setup)
  72 +- `frontend/src/test/authSlice.test.ts` — 创建
  73 +- `frontend/src/test/LoginPage.test.tsx` — 创建
  74 +
  75 +---
  76 +
  77 +## 任务步骤
  78 +
  79 +### Task 1: 后端项目骨架
  80 +
  81 +**Files:**
  82 +- 创建: `backend/pom.xml`
  83 +- 创建: `backend/src/main/java/com/example/erp/Application.java`
  84 +- 创建: `backend/src/main/resources/application.yml`
  85 +- 创建: `backend/src/main/resources/application-dev.yml`
  86 +- 创建: `backend/src/main/resources/db/migration/V1__initial_schema.sql`
  87 +- 测试: `backend/src/test/java/com/example/erp/ApplicationContextTest.java`
  88 +
  89 +**pom.xml 必须包含的依赖:**
  90 +- `spring-boot-starter-parent` 3.x(parent)
  91 +- `spring-boot-starter-web`, `spring-boot-starter-security`, `spring-boot-starter-validation`, `spring-boot-starter-test`
  92 +- `mybatis-plus-spring-boot3-starter` 3.5.x
  93 +- `mysql-connector-j`(runtime scope)
  94 +- `flyway-core` + `flyway-mysql`(10.x)
  95 +- `jjwt-api` + `jjwt-impl` + `jjwt-jackson`(0.12.x;impl/jackson 用 runtime scope)
  96 +- `lombok`(optional)
  97 +- `hutool-all` 5.8.x
  98 +
  99 +**application.yml 关键片段:**
  100 +```yaml
  101 +spring:
  102 + datasource:
  103 + url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_SCHEMA}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
  104 + username: ${DB_USER}
  105 + password: ${DB_PASSWORD}
  106 + driver-class-name: com.mysql.cj.jdbc.Driver
  107 + flyway:
  108 + locations: classpath:db/migration
  109 + baseline-on-migrate: true
  110 + baseline-version: 1
  111 +server:
  112 + port: 8080
  113 +jwt:
  114 + secret: ${JWT_SECRET}
  115 + access-token-expiry: 86400
  116 + refresh-token-expiry: 604800
  117 +```
  118 +
  119 +**application-dev.yml:**
  120 +```yaml
  121 +spring:
  122 + config:
  123 + import: optional:file:.env.local[.properties]
  124 +```
  125 +
  126 +**说明:** `.env.local` 含 `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_SCHEMA`, `JWT_SECRET`。dev profile 通过 `spring.config.import` 自动加载;`baseline-on-migrate=true` + `baseline-version=1` 让 Flyway 把已有 schema 标记为 V1 已应用,不重复执行。
  127 +
  128 +- [ ] **Step 1: 写失败测试**
  129 + - 测试名: `ApplicationContextTest#contextLoads`
  130 + - 意图: `@SpringBootTest` 启动 ApplicationContext 不报错(验证 Bean 配置和 DB 连接)
  131 + - 子会话确认 FAIL(项目不存在,无法编译)
  132 +
  133 +- [ ] **Step 2: 创建 Maven 项目结构**
  134 + - 创建目录树:`backend/src/main/java/com/example/erp/`、`backend/src/main/resources/db/migration/`、`backend/src/test/java/com/example/erp/`
  135 + - 写 pom.xml(含上述依赖)
  136 + - 写 Application.java(`@SpringBootApplication`,标准 main 方法)
  137 + - 写 application.yml + application-dev.yml(按上述片段)
  138 + - 复制 `sql/migrations/V1__initial_schema.sql` 内容到 `backend/src/main/resources/db/migration/V1__initial_schema.sql`
  139 + - 写 `ApplicationContextTest.java`(`@SpringBootTest`,空的 `contextLoads()` 方法)
  140 + - 写 `backend/src/test/java/com/example/erp/TestApplication.java`(继承 `Application`,供测试使用 dev profile)
  141 +
  142 +- [ ] **Step 3: 子会话运行 `cd backend && mvn test -Dspring.profiles.active=dev`,确认 contextLoads PASS**
  143 +
  144 +- [ ] **Step 4: Commit**
  145 + - `git add backend/`
  146 + - `git commit -m "chore(backend): init Spring Boot 3 project skeleton REQ-USR-004"`
  147 +
  148 +---
  149 +
  150 +### Task 2: Common 层(Result / BizException / ErrorCode / GlobalExceptionHandler)
  151 +
  152 +**Files:**
  153 +- 创建: `backend/src/main/java/com/example/erp/common/response/Result.java`
  154 +- 创建: `backend/src/main/java/com/example/erp/common/exception/BizException.java`
  155 +- 创建: `backend/src/main/java/com/example/erp/common/constants/AuthErrorCode.java`
  156 +- 创建: `backend/src/main/java/com/example/erp/common/exception/GlobalExceptionHandler.java`
  157 +- 测试: `backend/src/test/java/com/example/erp/common/ResultTest.java`
  158 +
  159 +**API shape:**
  160 +- `Result<T>` — 字段:`int code`,`String message`,`T data`,`long timestamp`
  161 + - `static <T> Result<T> ok(T data)` → code=200, message="操作成功", timestamp=System.currentTimeMillis()
  162 + - `static <T> Result<T> fail(int code, String message)` → data=null, timestamp=System.currentTimeMillis()
  163 +- `BizException(int code, String message)` — 继承 RuntimeException,含 `int code` 字段
  164 +- `GlobalExceptionHandler (@RestControllerAdvice)`:
  165 + - `handleBizException(BizException e)` → `Result.fail(e.getCode(), e.getMessage())`,HTTP 200
  166 + - `handleMethodArgumentNotValid(MethodArgumentNotValidException e)` → `Result.fail(40001, 首个字段错误信息)`,HTTP 200
  167 + - `handleException(Exception e)` → `Result.fail(99000, "系统内部错误")`,记 Logback error 级日志
  168 +
  169 +**合同级错误码常量(AuthErrorCode.java,`public static final int`):**
  170 +```java
  171 +USERNAME_OR_PASSWORD_ERROR = 40100 // 用户名或密码错误(不区分哪个,防枚举)
  172 +ACCOUNT_DISABLED = 40101 // 账号已被禁用,请联系管理员
  173 +ACCOUNT_LOCKED = 40102 // 账号已被锁定,请 N 分钟后重试
  174 +REFRESH_TOKEN_INVALID = 40103 // Refresh Token 已失效,请重新登录
  175 +```
  176 +
  177 +- [ ] **Step 1: 写失败测试**
  178 + - `ResultTest#ok_setsCode200AndData` — `Result.ok("hello").getCode() == 200 && "hello".equals(result.getData())`
  179 + - `ResultTest#fail_setsCodeAndNullData` — `Result.fail(40100, "msg").getCode() == 40100 && result.getData() == null`
  180 + - `ResultTest#ok_hasTimestamp` — `Result.ok(null).getTimestamp() > 0`
  181 + - 子会话确认 FAIL(类不存在)
  182 +
  183 +- [ ] **Step 2: 实现 Result.java + BizException.java + AuthErrorCode.java + GlobalExceptionHandler.java**
  184 +
  185 +- [ ] **Step 3: 子会话运行 `mvn test`,确认 3 个 ResultTest PASS**
  186 +
  187 +- [ ] **Step 4: Commit**
  188 + - `git commit -m "feat(usr): common Result/BizException/AuthErrorCode/GlobalExceptionHandler REQ-USR-004"`
  189 +
  190 +---
  191 +
  192 +### Task 3: JwtUtil
  193 +
  194 +**Files:**
  195 +- 创建: `backend/src/main/java/com/example/erp/config/JwtProperties.java`
  196 +- 创建: `backend/src/main/java/com/example/erp/common/util/JwtUtil.java`
  197 +- 测试: `backend/src/test/java/com/example/erp/common/JwtUtilTest.java`
  198 +
  199 +**API shape:**
  200 +- `JwtProperties (@ConfigurationProperties("jwt"))` — `String secret`,`long accessTokenExpiry`(秒),`long refreshTokenExpiry`(秒);加 `@EnableConfigurationProperties(JwtProperties.class)` 于 Application 或 Config 类
  201 +- `JwtUtil (@Component)`:注入 JwtProperties
  202 + - `generateAccessToken(String userId, String username, String userType, String brandId) : String`
  203 + - claims: sub=userId, "username"=username, "userType"=userType, "brandId"=brandId, exp=now + accessTokenExpiry 秒
  204 + - HMAC-SHA256,key = `Keys.hmacShaKeyFor(properties.getSecret().getBytes(StandardCharsets.UTF_8))`
  205 + - `generateRefreshToken(String userId, String brandId) : String`
  206 + - claims: sub=userId, "brandId"=brandId, "type"="refresh", exp=now + refreshTokenExpiry 秒
  207 + - `parseAccessToken(String token) : Claims`
  208 + - 若签名无效或过期 → throw `new BizException(AuthErrorCode.REFRESH_TOKEN_INVALID, "Token 已失效,请重新登录")`
  209 + - `parseRefreshToken(String token) : Claims`
  210 + - 解析同 parseAccessToken
  211 + - 验证 claim "type" == "refresh";否则 → throw BizException(40103)
  212 +
  213 +- [ ] **Step 1: 写失败测试**
  214 + - `JwtUtilTest#generateAndParseAccessToken_containsAllClaims`
  215 + - 构造 JwtUtil(properties: secret="testSecretKey32CharacterMinLength!", accessTokenExpiry=86400, refreshTokenExpiry=604800)
  216 + - 生成 access token,parseAccessToken → sub=="u1", username=="admin", userType=="超级管理员", brandId=="b1"
  217 + - `JwtUtilTest#parseRefreshToken_withAccessToken_throws40103`
  218 + - 生成 access token,传入 parseRefreshToken → 抛 BizException,code=40103
  219 + - `JwtUtilTest#parseAccessToken_withExpiredToken_throws40103`
  220 + - 构造 JwtUtil(accessTokenExpiry=-1 或 0),生成 token 后 parseAccessToken → 抛 BizException,code=40103
  221 + - 子会话确认 FAIL(类不存在)
  222 +
  223 +- [ ] **Step 2: 实现 JwtProperties + JwtUtil**
  224 + - JJWT 0.12.x API:`Jwts.builder()...signWith(key, Jwts.SIG.HS256).compact()`;解析:`Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload()`
  225 +
  226 +- [ ] **Step 3: 子会话运行 JwtUtilTest(3 个测试),确认 PASS**
  227 +
  228 +- [ ] **Step 4: Commit**
  229 + - `git commit -m "feat(usr): JwtUtil generate + parse access/refresh token REQ-USR-004"`
  230 +
  231 +---
  232 +
  233 +### Task 4: Entity + Mapper(BrandEntity / UsrUserEntity)
  234 +
  235 +**Files:**
  236 +- 创建: `backend/src/main/java/com/example/erp/module/usr/entity/BrandEntity.java`
  237 +- 创建: `backend/src/main/java/com/example/erp/module/usr/entity/UsrUserEntity.java`
  238 +- 创建: `backend/src/main/java/com/example/erp/module/usr/mapper/BrandMapper.java`
  239 +- 创建: `backend/src/main/java/com/example/erp/module/usr/mapper/UsrUserMapper.java`
  240 +- 创建: `backend/src/main/java/com/example/erp/config/MyBatisPlusConfig.java`
  241 +- 测试: `backend/src/test/java/com/example/erp/module/usr/BrandMapperTest.java`
  242 +
  243 +**API shape:**
  244 +- `BrandEntity (@TableName("brand"))` — 字段(均 `@TableField("<column_name>")`):`iIncrement`(@TableId,AUTO),`sId`,`sNo`,`sName`,`sShortName`,`sBrandsId`,`sSubsidiaryId`,`tCreateDate(LocalDateTime)`
  245 +- `UsrUserEntity (@TableName("usr_user"))` — 字段:`iIncrement`(@TableId,AUTO),`sId`,`sBrandsId`,`sSubsidiaryId`,`tCreateDate(LocalDateTime)`,`sUserCode`,`sUsername`,`sPasswordHash`,`sUserType`,`sLanguage`,`bCanEditDoc(Integer)`,`bIsDisabled(Integer)`,`sEmployeeId`,`sCreatorUsername`,`tLastLoginDate(LocalDateTime)`,`iLoginFailCount(Integer)`,`tLockUntil(LocalDateTime)`
  246 +- `BrandMapper extends BaseMapper<BrandEntity>`(无额外方法)
  247 +- `UsrUserMapper extends BaseMapper<UsrUserEntity>`(无额外方法;更新逻辑在 Service 用 LambdaUpdateWrapper 完成)
  248 +- `MyBatisPlusConfig (@Configuration)` — `@MapperScan("com.example.erp.module.*.mapper")`;注册 `MybatisPlusInterceptor` + `PaginationInnerInterceptor(DbType.MYSQL)`
  249 +
  250 +**测试(BrandMapperTest — @SpringBootTest,使用真实测试库):**
  251 +- `@BeforeEach` 插入 brand 行:`sId='b-test-001', sNo='TST', sName='测试版', iIncrement=null`(自增),其余字段留 null
  252 +- `@AfterEach` DELETE WHERE sNo='TST'
  253 +- 测试方法 `findByNo_returnsCorrectBrand` — `new LambdaQueryWrapper<BrandEntity>().eq(BrandEntity::getSNo, "TST")` selectOne → sName == "测试版"
  254 +
  255 +- [ ] **Step 1: 写失败测试**
  256 + - `BrandMapperTest#findByNo_returnsCorrectBrand`
  257 + - 子会话确认 FAIL(类不存在)
  258 +
  259 +- [ ] **Step 2: 实现 Entity + Mapper + MyBatisPlusConfig**
  260 + - 注意:Entity 字段名用 Java camelCase,`@TableField` 注解对应数据库实际列名(如 `@TableField("sBrandsId")` 或直接用 MyBatis-Plus 全局下划线转换——由于列名本身是驼峰,需关闭 `map-underscore-to-camel-case` 或手动 @TableField)
  261 +
  262 +- [ ] **Step 3: 子会话运行 BrandMapperTest,确认 PASS**
  263 +
  264 +- [ ] **Step 4: Commit**
  265 + - `git commit -m "feat(usr): BrandEntity/UsrUserEntity + Mapper REQ-USR-004"`
  266 +
  267 +---
  268 +
  269 +### Task 5: AuthService — 登录核心逻辑
  270 +
  271 +**Files:**
  272 +- 创建: `backend/src/main/java/com/example/erp/module/usr/dto/LoginReqDTO.java`
  273 +- 创建: `backend/src/main/java/com/example/erp/module/usr/vo/LoginVO.java`
  274 +- 创建: `backend/src/main/java/com/example/erp/module/usr/service/AuthService.java`
  275 +- 创建: `backend/src/main/java/com/example/erp/module/usr/service/impl/AuthServiceImpl.java`
  276 +- 创建: `backend/src/main/java/com/example/erp/config/BeanConfig.java`
  277 +- 测试: `backend/src/test/java/com/example/erp/module/usr/AuthServiceTest.java`
  278 +
  279 +**API shape:**
  280 +- `LoginReqDTO` — `@NotBlank String brandNo`,`@NotBlank String username`,`@NotBlank String password`
  281 +- `LoginVO` — `String accessToken`,`String refreshToken`,`long expiresIn`(固定值 86400),`UserInfoVO userInfo`
  282 + - `UserInfoVO (static inner class)` — `String userId`,`String username`,`String userType`,`String language`,`String brandId`
  283 +- `AuthService` — `LoginVO login(LoginReqDTO req)`;`String refresh(String refreshToken)`;`List<BrandVO> getBrands()`
  284 +- `AuthServiceImpl (@Service @Transactional)` — 注入 `BrandMapper`,`UsrUserMapper`,`JwtUtil`,`BCryptPasswordEncoder`
  285 +
  286 +**AuthServiceImpl.login 业务规则(按顺序):**
  287 +1. `brandMapper.selectOne(new LambdaQueryWrapper<BrandEntity>().eq(BrandEntity::getSNo, req.getBrandNo()))` → null → `throw new BizException(40100, "用户名或密码错误")`
  288 +2. `userMapper.selectOne(new LambdaQueryWrapper<UsrUserEntity>().eq(UsrUserEntity::getSUsername, req.getUsername()).eq(UsrUserEntity::getSBrandsId, brand.getSId()))` → null → `throw new BizException(40100, "用户名或密码错误")`
  289 +3. `user.getBIsDisabled() == 1` → `throw new BizException(40101, "账号已被禁用,请联系管理员")`
  290 +4. `user.getTLockUntil() != null && user.getTLockUntil().isAfter(LocalDateTime.now())` → 计算 remainMinutes = `(int) Math.ceil(ChronoUnit.SECONDS.between(LocalDateTime.now(), user.getTLockUntil()) / 60.0)` → `throw new BizException(40102, "账号已被锁定,请 " + remainMinutes + " 分钟后重试")`
  291 +5. `!passwordEncoder.matches(req.getPassword(), user.getSPasswordHash())` → `int newCount = user.getILoginFailCount() + 1`
  292 + - `newCount >= 5`: `LocalDateTime lockUntil = LocalDateTime.now().plusMinutes(30)`;`userMapper.update(null, new LambdaUpdateWrapper<UsrUserEntity>().eq(UsrUserEntity::getSId, user.getSId()).set(UsrUserEntity::getILoginFailCount, newCount).set(UsrUserEntity::getTLockUntil, lockUntil))` → `throw new BizException(40102, "账号已被锁定,请 30 分钟后重试")`
  293 + - `newCount < 5`: `userMapper.update(null, new LambdaUpdateWrapper<UsrUserEntity>().eq(UsrUserEntity::getSId, user.getSId()).set(UsrUserEntity::getILoginFailCount, newCount))` → `throw new BizException(40100, "用户名或密码错误")`
  294 +6. 成功:`userMapper.update(null, new LambdaUpdateWrapper<UsrUserEntity>().eq(UsrUserEntity::getSId, user.getSId()).set(UsrUserEntity::getILoginFailCount, 0).set(UsrUserEntity::getTLockUntil, null).set(UsrUserEntity::getTLastLoginDate, LocalDateTime.now()))`;签发 tokens;返回 LoginVO
  295 +
  296 +- [ ] **Step 1: 写 7 个失败单元测试(AuthServiceTest — @ExtendWith(MockitoExtension.class),mock BrandMapper/UsrUserMapper/JwtUtil/BCryptPasswordEncoder)**
  297 + - `login_brandNotFound_throws40100` — brandMapper.selectOne → null → BizException(40100)
  298 + - `login_userNotFound_throws40100` — brand 存在,userMapper.selectOne → null → BizException(40100)
  299 + - `login_accountDisabled_throws40101` — user.bIsDisabled=1 → BizException(40101)
  300 + - `login_accountLocked_throws40102WithRemainingMinutes` — user.tLockUntil=now+20min → BizException(40102),message 含 "20 分钟"
  301 + - `login_wrongPassword_firstTime_throws40100AndIncrementsCount` — BCrypt 不匹配,iLoginFailCount=0 → 更新为 1,throw 40100
  302 + - `login_wrongPassword_5thTime_setsLockAndThrows40102` — BCrypt 不匹配,iLoginFailCount=4 → 更新为 5,设 tLockUntil,throw 40102
  303 + - `login_success_resetsCountAndReturnsTokens` — BCrypt 匹配 → reset count,issue tokens,返回 LoginVO
  304 +
  305 +- [ ] **Step 2: 实现 LoginReqDTO + LoginVO + AuthService + AuthServiceImpl.login()**
  306 + - `BCryptPasswordEncoder` 注入:在 Task 5 中同步创建 `BeanConfig.java`(`@Configuration @Bean BCryptPasswordEncoder passwordEncoder()`),使 Spring context(ApplicationContextTest)在 Task 5 之后仍能正常加载;SecurityConfig(Task 7)不重复声明此 Bean
  307 +
  308 +- [ ] **Step 3: 子会话运行 AuthServiceTest(7 个测试),确认 PASS**
  309 +
  310 +- [ ] **Step 4: Commit**
  311 + - `git commit -m "feat(usr): AuthService.login multi-tenant + lockout logic REQ-USR-004"`
  312 +
  313 +---
  314 +
  315 +### Task 6: AuthService — refresh + getBrands
  316 +
  317 +**Files:**
  318 +- 创建: `backend/src/main/java/com/example/erp/module/usr/dto/RefreshTokenReqDTO.java`
  319 +- 创建: `backend/src/main/java/com/example/erp/module/usr/vo/BrandVO.java`
  320 +- 修改: `backend/src/main/java/com/example/erp/module/usr/service/impl/AuthServiceImpl.java`(新增 refresh + getBrands)
  321 +- 测试: `backend/src/test/java/com/example/erp/module/usr/AuthServiceTest.java`(追加 3 个测试方法)
  322 +
  323 +**API shape:**
  324 +- `RefreshTokenReqDTO` — `@NotBlank String refreshToken`
  325 +- `BrandVO` — `String sNo`,`String sName`
  326 +- `AuthServiceImpl#refresh(String refreshToken) : String`
  327 + 1. `Claims claims = jwtUtil.parseRefreshToken(refreshToken)` — 无效/过期自动抛 BizException(40103)
  328 + 2. `String userId = claims.getSubject(); String brandId = claims.get("brandId", String.class)`
  329 + 3. `UsrUserEntity user = userMapper.selectOne(new LambdaQueryWrapper<UsrUserEntity>().eq(UsrUserEntity::getSId, userId))` → null 或 `bIsDisabled=1` → throw BizException(40103, "Refresh Token 已失效,请重新登录")
  330 + 4. 签发新 accessToken:`jwtUtil.generateAccessToken(user.getSId(), user.getSUsername(), user.getSUserType(), brandId)`;返回新 accessToken 字符串
  331 +- `AuthServiceImpl#getBrands() : List<BrandVO>`
  332 + - `brandMapper.selectList(new QueryWrapper<BrandEntity>().select("sNo", "sName").orderByAsc("sName"))` → 映射为 BrandVO 列表
  333 +
  334 +- [ ] **Step 1: 写 3 个失败测试(追加到 AuthServiceTest)**
  335 + - `refresh_validRefreshToken_returnsNewAccessToken` — parseRefreshToken 成功,查库返回有效 user → generateAccessToken 被调用,返回新 token
  336 + - `refresh_invalidRefreshToken_throws40103` — parseRefreshToken 抛 BizException(40103)
  337 + - `getBrands_returnsListSortedByName` — brandMapper.selectList 返回 [b1, b2] → 结果 List<BrandVO> 包含对应 sNo/sName
  338 +
  339 +- [ ] **Step 2: 实现 RefreshTokenReqDTO + BrandVO + AuthServiceImpl#refresh() + AuthServiceImpl#getBrands()**
  340 +
  341 +- [ ] **Step 3: 子会话运行 AuthServiceTest(新增 3 个测试),确认 PASS**
  342 +
  343 +- [ ] **Step 4: Commit**
  344 + - `git commit -m "feat(usr): AuthService.refresh + getBrands REQ-USR-004"`
  345 +
  346 +---
  347 +
  348 +### Task 7: SecurityConfig + JwtAuthenticationFilter + AuthController
  349 +
  350 +**Files:**
  351 +- 创建: `backend/src/main/java/com/example/erp/config/SecurityConfig.java`
  352 +- 创建: `backend/src/main/java/com/example/erp/config/JwtAuthenticationFilter.java`
  353 +- 创建: `backend/src/main/java/com/example/erp/module/usr/controller/AuthController.java`
  354 +- 测试: `backend/src/test/java/com/example/erp/module/usr/AuthControllerTest.java`
  355 +
  356 +**API shape:**
  357 +- `SecurityConfig (@Configuration @EnableWebSecurity)`:
  358 + - `@Bean SecurityFilterChain`: `csrf().disable()`;`sessionManagement(STATELESS)`;`authorizeHttpRequests`: `permitAll` for `/api/auth/**`,其余 `authenticated`
  359 + - `addFilterBefore(JwtAuthenticationFilter, UsernamePasswordAuthenticationFilter)`
  360 + - 不再声明 BCryptPasswordEncoder @Bean(已在 BeanConfig.java 声明)
  361 +- `JwtAuthenticationFilter extends OncePerRequestFilter`:
  362 + - 读 `Authorization` header,提取 Bearer token
  363 + - `jwtUtil.parseAccessToken(token)` → 设 `UsernamePasswordAuthenticationToken` 入 SecurityContextHolder
  364 + - token 无效 → 不设 context(Spring Security 后续返回 401);请求路径匹配 `/api/auth/**` → 直接放行不解析
  365 +- `AuthController (@RestController @RequestMapping("/api/auth") @RequiredArgsConstructor)`:
  366 + - `@PostMapping("/login") Result<LoginVO> login(@Valid @RequestBody LoginReqDTO req)` → `Result.ok(authService.login(req))`
  367 + - `@PostMapping("/refresh") Result<Map<String,String>> refresh(@Valid @RequestBody RefreshTokenReqDTO req)` → `Result.ok(Map.of("accessToken", authService.refresh(req.getRefreshToken())))`
  368 + - `@GetMapping("/brands") Result<List<BrandVO>> brands()` → `Result.ok(authService.getBrands())`
  369 +
  370 +- [ ] **Step 1: 写 4 个 MockMvc 失败测试(AuthControllerTest — @WebMvcTest + @MockBean AuthService)**
  371 + - `login_wrongPassword_returns40100` — authService.login 抛 BizException(40100) → 响应 JSON code=40100
  372 + - `login_validCredentials_returns200AndTokens` — authService.login 返回 LoginVO → 响应 JSON code=200,accessToken 非空
  373 + - `refresh_invalidToken_returns40103` — authService.refresh 抛 BizException(40103) → code=40103
  374 + - `getBrands_returns200AndList` — authService.getBrands 返回 [BrandVO{sNo="STD", sName="标准版"}] → code=200,list 含该项
  375 +
  376 +- [ ] **Step 2: 实现 SecurityConfig + JwtAuthenticationFilter + AuthController**
  377 +
  378 +- [ ] **Step 3: 子会话运行 AuthControllerTest(4 个测试),确认 PASS**
  379 +
  380 +- [ ] **Step 4: 手动 smoke test(如果后端可本地运行)**
  381 + - `cd backend && mvn spring-boot:run -Dspring-boot.run.profiles=dev`(需 .env.local 在 backend/ 父目录可找到)
  382 + - `curl -s -X GET http://localhost:8080/api/auth/brands | jq .`
  383 + - `curl -s -X POST http://localhost:8080/api/auth/login -H "Content-Type: application/json" -d '{"brandNo":"STD","username":"admin","password":"666666"}' | jq .`
  384 +
  385 +- [ ] **Step 5: Commit**
  386 + - `git commit -m "feat(usr): SecurityConfig + JwtFilter + AuthController REQ-USR-004"`
  387 +
  388 +---
  389 +
  390 +### Task 8: 前端项目骨架
  391 +
  392 +**Files:**
  393 +- 创建: `frontend/package.json`
  394 +- 创建: `frontend/vite.config.ts`
  395 +- 创建: `frontend/tsconfig.json`
  396 +- 创建: `frontend/index.html`
  397 +- 创建: `frontend/src/main.tsx`
  398 +- 创建: `frontend/src/App.tsx`
  399 +- 创建: `frontend/src/styles/tokens.css`
  400 +
  401 +**package.json 关键依赖:**
  402 +```json
  403 +"dependencies": {
  404 + "react": "^18.3.0", "react-dom": "^18.3.0",
  405 + "antd": "^5.17.0", "@ant-design/icons": "^5.3.0",
  406 + "@reduxjs/toolkit": "^2.2.0", "react-redux": "^9.1.0",
  407 + "react-router-dom": "^6.23.0",
  408 + "axios": "^1.7.0", "dayjs": "^1.11.0"
  409 +},
  410 +"devDependencies": {
  411 + "vite": "^5.2.0", "@vitejs/plugin-react": "^4.3.0",
  412 + "typescript": "^5.4.0",
  413 + "@types/react": "^18.3.0", "@types/react-dom": "^18.3.0",
  414 + "vitest": "^1.6.0",
  415 + "@testing-library/react": "^15.0.0",
  416 + "@testing-library/jest-dom": "^6.4.0",
  417 + "@testing-library/user-event": "^14.5.0",
  418 + "jsdom": "^24.0.0"
  419 +}
  420 +```
  421 +
  422 +**vite.config.ts 关键配置:**
  423 +```ts
  424 +export default defineConfig({
  425 + plugins: [react()],
  426 + server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } },
  427 + test: { environment: 'jsdom', globals: true, setupFiles: './src/test/setup.ts' }
  428 +})
  429 +```
  430 +
  431 +**App.tsx 骨架:** `<BrowserRouter>` 包裹路由,`/login` → `<LoginPage />`,`/` → PrivateRoute(暂时重定向 /login,后续 REQ 补充),`*` → 404
  432 +
  433 +- [ ] **Step 1: 创建前端项目文件结构**
  434 + - `mkdir -p frontend/src/{styles,api,store/slices,pages/usr,test,hooks,components,utils}`
  435 + - 写 package.json(上述依赖)
  436 + - 写 vite.config.ts、tsconfig.json、index.html、src/main.tsx、src/App.tsx(骨架)
  437 + - 复制 `src/styles/tokens.css` 内容到 `frontend/src/styles/tokens.css`
  438 + - 写 `frontend/src/test/setup.ts`(`import "@testing-library/jest-dom"`)
  439 + - `cd frontend && npm install`
  440 +
  441 +- [ ] **Step 2: 验证骨架构建**
  442 + - `cd frontend && npm run build`(应成功,0 错误)
  443 +
  444 +- [ ] **Step 3: Commit**
  445 + - `git add frontend/`
  446 + - `git commit -m "chore(frontend): init Vite React project skeleton REQ-USR-004"`
  447 +
  448 +---
  449 +
  450 +### Task 9: 前端 Auth API 层 + Redux authSlice
  451 +
  452 +**Files:**
  453 +- 创建: `frontend/src/api/request.ts`
  454 +- 创建: `frontend/src/api/auth.ts`
  455 +- 创建: `frontend/src/store/index.ts`
  456 +- 创建: `frontend/src/store/slices/authSlice.ts`
  457 +- 测试: `frontend/src/test/authSlice.test.ts`
  458 +
  459 +**API shape:**
  460 +- `request.ts` — Axios 实例 `baseURL: '/api'`,`timeout: 10000`
  461 + - 请求拦截器:从 `store.getState().auth.accessToken` 读 token,注入 `Authorization: Bearer ${token}`
  462 + - 响应拦截器(成功):`response.data.code !== 200` → 抛 `new Error(response.data.message)`
  463 + - 响应拦截器(HTTP 401):调 `auth.refresh(store.getState().auth.refreshToken)` → 更新 store → 重试原请求;refresh 失败 → `store.dispatch(clearCredentials())` → `window.location.href = '/login'`
  464 +- `auth.ts`:
  465 + - `login(params: { brandNo: string; username: string; password: string }) : Promise<LoginVO>` — POST /api/auth/login
  466 + - `refresh(refreshToken: string) : Promise<{ accessToken: string }>` — POST /api/auth/refresh
  467 + - `getBrands() : Promise<BrandVO[]>` — GET /api/auth/brands
  468 + - 类型定义(TypeScript interface):`LoginVO`(含 accessToken, refreshToken, expiresIn, userInfo: UserInfoVO),`UserInfoVO`(userId, username, userType, language, brandId),`BrandVO`(sNo, sName)
  469 +- `authSlice` — `createSlice({ name: 'auth', initialState: { accessToken: null, refreshToken: null, userInfo: null } })`
  470 + - `setCredentials(state, action: PayloadAction<{ accessToken: string; refreshToken: string; userInfo: UserInfoVO }>)` — 更新三字段
  471 + - `clearCredentials(state)` — 三字段重置 null
  472 +- `store/index.ts` — `configureStore({ reducer: { auth: authReducer } })`;导出 `RootState`、`AppDispatch`
  473 +
  474 +- [ ] **Step 1: 写 2 个失败单元测试(authSlice.test.ts)**
  475 + - `setCredentials_updatesAllStateFields` — dispatch setCredentials({accessToken:"t1", refreshToken:"r1", userInfo:{userId:"u1",...}}) → state.auth.accessToken=="t1"
  476 + - `clearCredentials_resetsToNull` — dispatch clearCredentials → state.auth.accessToken==null
  477 +
  478 +- [ ] **Step 2: 实现 request.ts + auth.ts + authSlice + store/index.ts**
  479 +
  480 +- [ ] **Step 3: 子会话运行 `cd frontend && npm run test -- --run`,确认 authSlice 2 个测试 PASS**
  481 +
  482 +- [ ] **Step 4: Commit**
  483 + - `git commit -m "feat(usr): auth API layer + Redux authSlice REQ-USR-004"`
  484 +
  485 +---
  486 +
  487 +### Task 10: 登录页 LoginPage.tsx
  488 +
  489 +**Files:**
  490 +- 创建: `frontend/src/pages/usr/LoginPage.tsx`
  491 +- 修改: `frontend/src/App.tsx`(添加 /login 路由,PrivateRoute 守卫读 authSlice)
  492 +- 测试: `frontend/src/test/LoginPage.test.tsx`
  493 +
  494 +**UI 规范(来自 docs/06 § 五):**
  495 +- 版本 `Select`(label="公司/版本",name="brandNo"):组件 mount 时调 `getBrands()`,填充选项(value=sNo, label=sName);defaultValue = sName=="标准版" 对应的 sNo;若无"标准版"则选第一项
  496 +- 用户名 `Input`(label="用户名",name="username"):必填
  497 +- 密码 `Input.Password`(label="密码",name="password"):必填
  498 +- 提交按钮(text="登录",`loading={loading}`):防重复点击
  499 +- 失败:`message.error(接口返回 message)`
  500 +- 成功:`dispatch(setCredentials({accessToken, refreshToken, userInfo}))` → `navigate('/')`
  501 +
  502 +**LoginPage 内部数据流:**
  503 +1. `useEffect(() => { getBrands().then(setBrandOptions) }, [])` — 挂载时获取 brand 列表
  504 +2. `onFinish(values)` → `setLoading(true)` → `await login(values)` → `dispatch(setCredentials(...))` → `navigate('/')` → `finally setLoading(false)`
  505 +
  506 +- [ ] **Step 1: 写 2 个失败测试(LoginPage.test.tsx)**
  507 + - `renders_brandSelect_username_and_password_fields`
  508 + - render LoginPage(with Redux Provider + MemoryRouter + mock getBrands → [{sNo:"STD",sName:"标准版"}])
  509 + - 断言:combobox(brand Select)存在,`getByPlaceholderText` 或 label "用户名" Input 存在,"密码" Input 存在,"登录" Button 存在
  510 + - `submit_withValidCredentials_dispatchesSetCredentials`
  511 + - mock `auth.login` 返回 `{accessToken:"at", refreshToken:"rt", expiresIn:86400, userInfo:{userId:"u1",username:"admin",userType:"超级管理员",language:"中文",brandId:"b1"}}`
  512 + - 填写 username + password,点击登录 → `store.getState().auth.accessToken == "at"`
  513 +
  514 +- [ ] **Step 2: 实现 LoginPage.tsx + 更新 App.tsx(/login 路由)**
  515 +
  516 +- [ ] **Step 3: 子会话运行 `npm run test -- --run`,确认所有前端测试(包括 authSlice + LoginPage)PASS**
  517 +
  518 +- [ ] **Step 4: Commit**
  519 + - `git commit -m "feat(usr): LoginPage brand selector + login form REQ-USR-004"`
  520 +
  521 +---
  522 +
  523 +## 提交计划
  524 +
  525 +| 提交消息 | 覆盖 Task |
  526 +|---|---|
  527 +| `chore(backend): init Spring Boot 3 project skeleton REQ-USR-004` | Task 1 |
  528 +| `feat(usr): common Result/BizException/AuthErrorCode/GlobalExceptionHandler REQ-USR-004` | Task 2 |
  529 +| `feat(usr): JwtUtil generate + parse access/refresh token REQ-USR-004` | Task 3 |
  530 +| `feat(usr): BrandEntity/UsrUserEntity + Mapper REQ-USR-004` | Task 4 |
  531 +| `feat(usr): AuthService.login multi-tenant + lockout logic REQ-USR-004` | Task 5 |
  532 +| `feat(usr): AuthService.refresh + getBrands REQ-USR-004` | Task 6 |
  533 +| `feat(usr): SecurityConfig + JwtFilter + AuthController REQ-USR-004` | Task 7 |
  534 +| `chore(frontend): init Vite React project skeleton REQ-USR-004` | Task 8 |
  535 +| `feat(usr): auth API layer + Redux authSlice REQ-USR-004` | Task 9 |
  536 +| `feat(usr): LoginPage brand selector + login form REQ-USR-004` | Task 10 |
... ...
docs/superpowers/reviews/2026-05-08-REQ-USR-001.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-001
  3 +date: 2026-05-08
  4 +round: 3
  5 +reviewer: superpower-code-reviewer
  6 +---
  7 +
  8 +# Review: REQ-USR-001 — round 3
  9 +
  10 +## 结论
  11 +approve
  12 +
  13 +## Must-fix
  14 +(无)
  15 +
  16 +## Nice-to-have
  17 +- docs/05-API接口契约.md — GET /api/usr/users/staffs 和 GET /api/usr/users/permission-groups 两个接口未写入 docs/05,规格已标注「不在原始清单」但未补录,建议在 module-report 阶段补充
  18 +- UserServiceImpl.java — CLAUDE.md §编码行为约束 第 4 条要求关键方法带 REQ-USR-001 注释标签,当前后端文件均缺失(仅 PermButton.tsx 有一处)
  19 +
  20 +## 反例 / 测试覆盖缺口
  21 +全部三轮 must-fix 已正确修复并通过 41 用例验证。实现与 spec 完全对齐:权限校验、唯一性检查、EMPLOYEE_NOT_FOUND=40001、批量 insert(Collection)、@Transactional 边界、UserPrincipal 基础设施均符合规格。两个 nice-to-have 均为文档/注释层面的缺失,不影响功能正确性。
... ...
docs/superpowers/reviews/2026-05-08-REQ-USR-002.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-002
  3 +date: 2026-05-08
  4 +round: 2
  5 +reviewer: superpower-code-reviewer
  6 +---
  7 +
  8 +# Review: REQ-USR-002 — round 2
  9 +
  10 +## 结论
  11 +approve
  12 +
  13 +## Must-fix
  14 +(无)
  15 +
  16 +Round 1 全部 must-fix 已正确修复:
  17 +- M1 ✓ `updateUser_selfAdminKeepType_doesNotThrow` 测试已添加(含 userMapper.update stub)
  18 +- M2 ✓ `UserUpdateReqDTO` 已添加 @NotBlank/@Pattern/@NotNull 校验注解
  19 +- M3 ✓ `UserFormDrawer.InitialData` 已添加 permGroupIds 字段,useEffect 用 `?? []` 初始化
  20 +- M4 ✓ `UserListItemVO` 已添加 bCanEditDoc,mapper SELECT 已加列,UserListPage 传真实值
  21 +
  22 +## Nice-to-have
  23 +- backend/.../vo/UserUpdateRespVO.java — updatedAt 建议添加 @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss") 符合 docs/04 § 3.3 规范
  24 +- frontend/src/test/UserListPage.test.tsx:106 — editMode_submit_callsUpdateUser 可补 toHaveBeenCalledWith('u1', ...) 断言 userId 参数正确
  25 +- frontend/src/pages/usr/UserListPage.tsx:121-128 — initialData 未传 permGroupIds(因列表 API 无此字段),与规格 line 103 说明一致,属有意为之,建议加注释说明
  26 +
  27 +## 反例 / 测试覆盖缺口
  28 +- controller 层 BizException 40300/40400/40301 → 错误码 JSON 映射缺测试(低风险,handler 已在 createUser 路径覆盖)
  29 +- happy-path 未用 ArgumentCaptor 断言 bCanEditDoc/bIsDisabled 映射为正确 int 值(低风险)
... ...
docs/superpowers/reviews/2026-05-08-REQ-USR-003.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-003
  3 +date: 2026-05-08
  4 +round: 4
  5 +reviewer: superpower-code-reviewer
  6 +---
  7 +
  8 +# Review: REQ-USR-003 — round 4
  9 +
  10 +## 结论
  11 +approve
  12 +
  13 +## Must-fix
  14 +(无)
  15 +
  16 +## Nice-to-have
  17 +- backend/src/main/java/com/example/erp/module/usr/controller/UserController.java:35-50 — docs/05 声明了 Permission: usr:query,但当前 SecurityConfig 仅做鉴权(authenticated),未启用 @PreAuthorize,属跨切面 REQ 的存量缺口,非本 REQ 引入
  18 +- backend/src/main/java/com/example/erp/common/response/Result.java:19 — docs/04 § 1.3 示例写 code:0,实际实现与 docs/05 一致为 200;docs/04 示例是历史遗留,可单独 docs commit 修正
  19 +- backend/src/main/resources/mapper/UsrUserMapper.xml:29,43 — staffName/department equals 分支未加 IS NULL OR,因为 equals '部门名' 语义上不应匹配无职员用户,行为正确但可补注释说明
  20 +- backend/src/test/java/com/example/erp/module/usr/UserControllerTest.java:109 — getUsers_withToken_returns200 未用 ArgumentCaptor 验证五个参数均正确转发给 service
  21 +
  22 +## 反例 / 测试覆盖缺口
  23 +- 无 XML 动态查询路径(含/不含/等于、disabled 是否、lastLoginDate DATE 过滤、pageSize cap)的集成/切片测试;mapper 被 mock 后 XML 逻辑未被单元覆盖
  24 +- UserControllerTest 未断言响应中不含 sPasswordHash / iLoginFailCount / tLockUntil
  25 +- 前端测试未覆盖翻页 onChange 回调(load 以正确页码调用)
... ...
docs/superpowers/reviews/2026-05-08-REQ-USR-004.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-004
  3 +date: 2026-05-08
  4 +round: 2
  5 +reviewer: superpower-code-reviewer
  6 +verdict: approve
  7 +---
  8 +
  9 +# Review: REQ-USR-004 — round 2
  10 +
  11 +## 结论
  12 +approve
  13 +
  14 +## Round 1 Must-fix 修复确认
  15 +
  16 +- [x] **[high] uk_usr_user_username 多租户唯一索引** — 已新增 `sql/migrations/V2__fix_username_unique_per_tenant.sql`,DROP 旧索引,建立 `uk_usr_user_username_tenant (sUsername, sBrandsId)` 复合唯一索引;同步更新 `docs/03`;V2 已复制至 `backend/src/main/resources/db/migration/` Flyway 可应用路径。
  17 +- [x] **[medium] UpdateWrapper → LambdaUpdateWrapper** — `AuthServiceImpl.java` 全部 `UpdateWrapper` 改为 `LambdaUpdateWrapper`,使用方法引用(`UsrUserEntity::getSId` 等),编译期类型安全,单元测试通过。
  18 +- [x] **[medium] refresh() 锁定账号检查** — 追加 `tLockUntil` 检查分支;新增 `refresh_lockedUser_throws40103` 测试,验证锁定用户刷新 token 抛 40103。
  19 +
  20 +## 本轮新增 Nice-to-have(不阻塞通过)
  21 +
  22 +- `docs/05` 中 40400 行仍存在;属文档小偏差,不影响运行时行为,后续 docs 统一整理。
  23 +- `JwtUtil.doParse()` access/refresh 共用 40103 错误码,低优先级语义优化。
  24 +
  25 +## 测试结果
  26 +
  27 +**后端**(`JAVA_HOME=openjdk@21 mvn verify`):11 tests — 11 passed, 0 failed, 0 skipped
  28 +**前端**(`npm run test -- --run`):3 tests — 3 passed, 0 failed
  29 +
  30 +## 覆盖确认
  31 +
  32 +| 验收标准 | 覆盖 |
  33 +|---|---|
  34 +| 正确凭据返回 Access + Refresh Token | ✓ login_success_resetsCountAndReturnsTokens |
  35 +| 错误密码返回 40100,不区分用户名/密码 | ✓ login_wrongPassword_firstTime_throws40100 |
  36 +| 品牌不存在返回 40100 | ✓ login_brandNotFound_throws40100 |
  37 +| 5 次失败锁定 30 分钟 | ✓ login_wrongPassword_5thTime_setsLockAndThrows40102 |
  38 +| 锁定账号返回 40102 + 剩余分钟数 | ✓ login_accountLocked_throws40102WithRemainingMinutes |
  39 +| 禁用账号返回 40101 | ✓ login_accountDisabled_throws40101 |
  40 +| Refresh Token 正常换新 Access Token | ✓ refresh_validRefreshToken_returnsNewAccessToken |
  41 +| 无效 Refresh Token 返回 40103 | ✓ refresh_invalidRefreshToken_throws40103 |
  42 +| 锁定账号的 Refresh Token 失效 | ✓ refresh_lockedUser_throws40103 |
  43 +| 前端渲染版本下拉 + 默认选"标准版" | ✓ LoginPage 渲染测试 |
  44 +| 前端提交调用 login API | ✓ LoginPage 提交测试 |
... ...
docs/superpowers/specs/2026-05-08-REQ-USR-001.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-001
  3 +date: 2026-05-08
  4 +module: module_usr
  5 +---
  6 +
  7 +# Spec: REQ-USR-001 — 增加用户
  8 +
  9 +## 目标
  10 +
  11 +超级管理员在用户管理页新建用户账号,填写基本信息并分配权限组,账号保存后立即生效,可使用初始密码 666666 登录系统。
  12 +
  13 +## 输入 / 触发
  14 +
  15 +**触发**:超级管理员点击用户管理页「新增」按钮,弹出 Drawer,填写表单并提交。
  16 +
  17 +**POST /api/usr/users 请求体**:
  18 +```json
  19 +{
  20 + "userCode": "string(必填,用户号)",
  21 + "username": "string(必填,用户名,同一 brand 内唯一)",
  22 + "userType": "普通用户|超级管理员(必填)",
  23 + "language": "中文|英文|繁体(必填)",
  24 + "canEditDoc": false,
  25 + "employeeId": "string|null(可选,tStaff.sId)",
  26 + "permGroupIds": ["string"]
  27 +}
  28 +```
  29 +
  30 +**前端 Drawer 表单字段**:
  31 +| 字段 | 组件 | 必填 | 来源/选项 |
  32 +|---|---|---|---|
  33 +| 用户号 | Input | 是 | 手工输入 |
  34 +| 用户名 | Input | 是 | 手工输入 |
  35 +| 类型 | Select | 是 | 普通用户 / 超级管理员 |
  36 +| 语言 | Select | 是 | 中文 / 英文 / 繁体 |
  37 +| 单据修改权限 | Checkbox | 否 | 默认不勾 |
  38 +| 员工 | Select | 否 | GET /api/usr/staffs 返回列表,label=sStaffName, value=sId |
  39 +| 权限组 | Table+Checkbox | 否 | GET /api/usr/permission-groups 返回列表,列:权限分类/权限名称 |
  40 +
  41 +## 输出 / 结果
  42 +
  43 +**成功响应**:
  44 +```json
  45 +{
  46 + "code": 200,
  47 + "message": "操作成功",
  48 + "data": {
  49 + "userId": "string",
  50 + "userCode": "string",
  51 + "username": "string"
  52 + }
  53 +}
  54 +```
  55 +
  56 +前端收到成功响应后:`message.success("新增用户成功")` → 关闭 Drawer。
  57 +
  58 +## 业务规则
  59 +
  60 +1. **权限控制**:请求者 JWT 中 `userType == 超级管理员`,否则抛 `BizException(40300, "权限不足")`。
  61 +2. **唯一性检查(同 brand 内)**:
  62 + - `sUserCode` 全库唯一(索引 `uk_usr_user_usercode`);重复抛 `BizException(40902, "用户号已存在")`。
  63 + - `sUsername` 在同一 `sBrandsId` 内唯一(索引 `uk_usr_user_username_tenant`);重复抛 `BizException(40901, "用户名已存在")`。
  64 +3. **密码初始化**:`sPasswordHash = BCryptPasswordEncoder.encode("666666")`,禁止存明文。
  65 +4. **系统字段自动填充**(Controller 或 Service 层注入 `UserPrincipal`):
  66 + - `sId` = `UUID.randomUUID().toString()`
  67 + - `sBrandsId` = JWT claim `brandId`
  68 + - `sCreatorUsername` = JWT claim `username`
  69 + - `tCreateDate` = `LocalDateTime.now()`
  70 + - `bIsDisabled` = 0
  71 + - `iLoginFailCount` = 0
  72 +5. **employeeId 校验**:若不为 null,需验证 `tStaff.sId == employeeId && tStaff.sBrandsId == brandId`;不存在抛 `BizException(40001, "员工不存在")`。
  73 +6. **权限组写入**:`permGroupIds` 非空时,批量插入 `usr_user_permission`(`sUserId=新用户 sId`, `sPermGroupId=item`);`permGroupIds` 为空/null 时跳过。
  74 +7. **事务**:`createUser()` 方法标注 `@Transactional`,usr_user 插入 + usr_user_permission 批量插入在同一事务中。
  75 +
  76 +## 边界与约束
  77 +
  78 +- 初始密码仅后端生成,不通过请求体传入,前端不展示。
  79 +- `sUsername` 一旦创建不可修改(REQ-USR-002 约束)。
  80 +- `userType` 枚举只有 `普通用户` 和 `超级管理员` 两种合法值;`language` 枚举只有 `中文`、`英文`、`繁体`;后端用 `@Pattern`/`@NotNull` + `@Valid` 校验,违规触发 `40001`。
  81 +- 跨模块改动:需修改 `JwtAuthenticationFilter.java`(config 层),将 JWT claims 包装为 `UserPrincipal` 存入 SecurityContext,使 `UserController` 可通过 `@AuthenticationPrincipal UserPrincipal` 取到 `brandId` 和 `username`。此修改属必要基础设施扩展(CLAUDE.md S2)。
  82 +
  83 +## 依赖的 schema 表 / 字段
  84 +
  85 +**写入表**:
  86 +- `usr_user`:`sId`、`sBrandsId`、`sCreatorUsername`、`tCreateDate`、`sUserCode`、`sUsername`、`sPasswordHash`、`sUserType`、`sLanguage`、`bCanEditDoc`、`bIsDisabled`、`sEmployeeId`、`iLoginFailCount`
  87 +- `usr_user_permission`:`sId`(UUID)、`sBrandsId`、`tCreateDate`、`sUserId`、`sPermGroupId`
  88 +
  89 +**只读表**:
  90 +- `tStaff`:`sId`、`sStaffName`(员工下拉列表及 employeeId 校验)
  91 +- `usr_permission_group`:`sId`、`sGroupCode`、`sGroupName`、`sCategory`(权限组复选列表)
  92 +
  93 +## 依赖的接口
  94 +
  95 +- `POST /api/auth/login`(REQ-USR-004)— 前端登录获取 JWT,携带 Bearer Token 才可调用本接口
  96 +- `GET /api/usr/staffs`(本 REQ 新增,不在 docs/05 原始清单)— 员工下拉数据源,鉴权接口
  97 + - 响应:`{ "data": [{ "sId": "...", "sStaffName": "..." }] }`
  98 +- `GET /api/usr/permission-groups`(本 REQ 新增,不在 docs/05 原始清单)— 权限组复选数据源,鉴权接口
  99 + - 响应:`{ "data": [{ "sId": "...", "sGroupCode": "...", "sGroupName": "...", "sCategory": "..." }] }`
  100 +- `POST /api/usr/users`(docs/05 已定义)— 本 REQ 核心创建接口
  101 +
  102 +## 验收标准
  103 +
  104 +1. 超级管理员提交合法数据 → HTTP 200,`data.userId` 非空,数据库 `usr_user` 表新增一条记录,密码字段为 BCrypt 哈希(不含 "666666" 明文)。
  105 +2. 新建用户可立即用 POST /api/auth/login(brandNo + username + "666666")登录成功。
  106 +3. 重复用户名(同 brand)→ 返回 `code=40901`,用户名已存在。
  107 +4. 重复用户号 → 返回 `code=40902`,用户号已存在。
  108 +5. 普通用户调用本接口 → 返回 `code=40300`,权限不足。
  109 +6. 缺必填字段(userCode / username / userType / language 为空)→ 返回 `code=40001`。
  110 +7. 选中权限组后提交 → `usr_user_permission` 表有对应关联记录。
  111 +8. 关联无效 employeeId → 返回 `code=40001`,员工不存在。
  112 +9. 前端:新增 Drawer 表单加载时,员工下拉和权限组 Table 已从对应 API 拉取数据;提交成功后 Drawer 关闭并显示 `message.success`。
  113 +10. 前端:普通用户登录后,用户管理页「新增」按钮不显示(`PermButton permission="usr:create"` 隐藏逻辑)。
... ...
docs/superpowers/specs/2026-05-08-REQ-USR-002.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-002
  3 +date: 2026-05-08
  4 +module: usr
  5 +---
  6 +
  7 +# Spec: REQ-USR-002 — 修改用户
  8 +
  9 +## 目标
  10 +超级管理员可通过 userId 定位已有用户,更新其用户类型、语言、单据修改权限、启/停用状态、关联员工和权限组,修改立即生效;用户名不可修改,密码由单独接口重置。
  11 +
  12 +## 输入 / 触发
  13 +
  14 +HTTP 请求:`PUT /api/usr/users/{userId}`(需 Bearer Token 鉴权)
  15 +
  16 +Path 参数:`userId`(`usr_user.sId`,目标用户的业务 ID)
  17 +
  18 +Request Body(JSON):
  19 +
  20 +| 字段 | 类型 | 必填 | 说明 |
  21 +|---|---|---|---|
  22 +| userType | String | 是 | `普通用户` 或 `超级管理员` |
  23 +| language | String | 是 | `中文` / `英文` / `繁体` |
  24 +| canEditDoc | boolean | 是 | 是否有单据修改权限 |
  25 +| isDisabled | boolean | 是 | true = 禁用账号 |
  26 +| employeeId | String\|null | 否 | 关联职员 sId,null = 取消关联 |
  27 +| permGroupIds | String[] | 是 | 权限组 sId 列表(空数组 = 清空所有权限) |
  28 +
  29 +## 输出 / 结果
  30 +
  31 +HTTP 200,`Result<UserUpdateRespVO>`
  32 +
  33 +```json
  34 +{
  35 + "code": 200,
  36 + "data": {
  37 + "userId": "string",
  38 + "username": "string",
  39 + "updatedAt": "2026-05-08T10:00:00"
  40 + }
  41 +}
  42 +```
  43 +
  44 +`updatedAt` 为服务端处理时的 `LocalDateTime.now()`,序列化为 ISO-8601。
  45 +
  46 +## 业务规则
  47 +
  48 +1. **权限校验**:`principal.userType() != "超级管理员"` → 抛 BizException(40300),与 createUser 逻辑一致(来源:`UserServiceImpl.createUser`)
  49 +2. **用户存在性**:按 `sId = userId AND sBrandsId = brandId` 查 `usr_user`;找不到 → 抛 BizException(40400)
  50 +3. **自身角色保护**:`principal.userId().equals(userId)` 时,禁止将 `userType` 从 `超级管理员` 改为其他值 → 抛 BizException(40301)(防止唯一管理员自我降权导致系统锁死;即使目标 userType 已是普通用户也拦截,保持规则简单)
  51 +4. **employeeId 校验**:若 `employeeId != null`,需确认 `tStaff.sId = employeeId AND sBrandsId = brandId` 存在;不存在 → 抛 BizException(40001)
  52 +5. **更新字段**:仅更新 `sUserType / sLanguage / bCanEditDoc / bIsDisabled / sEmployeeId`;`sUsername / sUserCode / sPasswordHash / sCreatorUsername / tCreateDate` 不修改
  53 +6. **权限组 replace 策略**:先 `DELETE FROM usr_user_permission WHERE sUserId = userId AND sBrandsId = brandId`,再批量 INSERT 新的关联记录(与 createUser 的 permGroupIds 处理方式一致);permGroupIds 为空数组时仅删除,不插入
  54 +7. **多租户隔离**:所有查询和写入均带 `sBrandsId = principal.brandId()`
  55 +
  56 +## 边界与约束
  57 +
  58 +- 接口为写操作,需加 `@Transactional`
  59 +- 鉴权失败返回 401(SecurityConfig 已配置)
  60 +- `userType` / `language` 仅接受指定枚举值;违反 → 由 `@Valid` + `@NotNull` 在 DTO 层拦截返回 40001
  61 +- 密码不在此接口处理,`sPasswordHash` 保持原值不变
  62 +- 响应 JSON 不返回 sPasswordHash / iLoginFailCount / tLockUntil
  63 +
  64 +## 依赖的 schema 表 / 字段
  65 +
  66 +- `usr_user (别名 u)`:sId, sUserType, sLanguage, bCanEditDoc, bIsDisabled, sEmployeeId, sBrandsId(查找 + 更新)
  67 +- `tStaff`:sId, sBrandsId(校验 employeeId 存在性)
  68 +- `usr_permission_group`:仅前端下拉,不涉及后端写操作
  69 +- `usr_user_permission`:sUserId, sPermGroupId, sBrandsId, sId, tCreateDate(先删后插)
  70 +
  71 +## 依赖的接口
  72 +
  73 +- `POST /api/auth/login`(REQ-USR-004,获取 Bearer Token)
  74 +- `GET /api/usr/users`(REQ-USR-003,获取列表后点击行触发修改抽屉)
  75 +- `GET /api/usr/users/staffs`(REQ-USR-001,前端抽屉加载员工下拉)
  76 +- `GET /api/usr/users/permission-groups`(REQ-USR-001,前端抽屉加载权限组列表)
  77 +
  78 +## 验收标准
  79 +
  80 +1. `PUT /api/usr/users/{validId}` Body 合法 → HTTP 200,返回 userId/username/updatedAt,数据库对应字段已更新
  81 +2. `userId` 不存在(或属于其他 brand)→ 返回 40400
  82 +3. 非超级管理员 Token 调用 → 返回 40300
  83 +4. 超级管理员修改自己的 userType → 返回 40301
  84 +5. `isDisabled=true` 修改后,目标用户再登录 → 返回 40101(账号已禁用)
  85 +6. permGroupIds 为空数组 → 该用户权限组被清空,下次查询 permission-groups 对应关联为空
  86 +7. permGroupIds 非空 → 权限组先删后插,关联记录与提交值一致
  87 +8. 无 Token → 返回 401
  88 +9. 前端:UserListPage 表格行有"修改"按钮,点击弹出预填修改抽屉;提交后关闭抽屉并刷新列表
  89 +
  90 +## 前端交互设计
  91 +
  92 +**UserFormDrawer 改造(复用已有组件)**:
  93 +- 新增 props:`userId?: string`,`initialData?: { userType, language, canEditDoc, isDisabled, employeeId, permGroupIds: string[] }`
  94 +- 当 `userId` 存在时(edit 模式):
  95 + - 抽屉标题改为 "修改用户"
  96 + - 隐藏用户号和用户名 Form.Item(或显示为只读文本)
  97 + - `open` 时用 `initialData` 初始化 form 和 selectedPermIds
  98 + - 提交调 `updateUser(userId, req)` 而非 `createUser`
  99 +- 当 `userId` 不存在时(create 模式):行为与现有完全一致
  100 +
  101 +**UserListPage 改造**:
  102 +- columns 末尾加"操作"列,包含"修改"按钮(PermButton,权限 `usr:edit`)
  103 +- 点击"修改"按钮:将该行 `UserListItemVO` 转换为 `initialData`(permGroupIds 需额外从后端获取或在列表中推导——因列表不返回 permGroupIds,修改抽屉打开时无法预填权限组复选框,初始化为空数组即可,用户可重新勾选)
  104 +- 新增状态:`editingUser: UserListItemVO | null`,控制修改抽屉开关
  105 +
  106 +**新增 API 函数**(`frontend/src/api/usr.ts`):
  107 +```ts
  108 +export interface UserUpdateReq {
  109 + userType: string
  110 + language: string
  111 + canEditDoc: boolean
  112 + isDisabled: boolean
  113 + employeeId: string | null
  114 + permGroupIds: string[]
  115 +}
  116 +
  117 +export interface UserUpdateResp {
  118 + userId: string
  119 + username: string
  120 + updatedAt: string
  121 +}
  122 +
  123 +export function updateUser(userId: string, req: UserUpdateReq): Promise<UserUpdateResp> {
  124 + return request.put(`/usr/users/${userId}`, req)
  125 +}
  126 +```
... ...
docs/superpowers/specs/2026-05-08-REQ-USR-003.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-003
  3 +date: 2026-05-08
  4 +module: usr
  5 +---
  6 +
  7 +# Spec: REQ-USR-003 — 查询用户
  8 +
  9 +## 目标
  10 +超级管理员可按 8 种查询字段 + 3 种匹配方式组合筛选,分页浏览本品牌下的用户列表;列表同时展示关联职员的员工名与部门。
  11 +
  12 +## 输入 / 触发
  13 +HTTP 请求:`GET /api/usr/users`(需 Bearer Token 鉴权)
  14 +
  15 +Query 参数:
  16 +
  17 +| 参数 | 类型 | 必填 | 默认值 | 说明 |
  18 +|---|---|---|---|---|
  19 +| queryField | String | 否 | `username` | 查询字段枚举,见下表 |
  20 +| matchType | String | 否 | `contains` | 匹配方式枚举:`contains` / `notContains` / `equals` |
  21 +| queryValue | String | 否 | — | 查询值;空字符串或不传 = 全部 |
  22 +| page | int | 否 | 1 | 页码,从 1 开始 |
  23 +| pageSize | int | 否 | 20 | 每页条数,最大 100 |
  24 +
  25 +queryField 枚举(前端显示名 → 参数值 → 后端列):
  26 +
  27 +| 前端显示 | 参数值 | 后端列 |
  28 +|---|---|---|
  29 +| 用户名 | `username` | `u.sUsername` |
  30 +| 员工名 | `staffName` | `s.sStaffName` |
  31 +| 用户号 | `userCode` | `u.sUserCode` |
  32 +| 部门 | `department` | `s.sDepartment` |
  33 +| 用户类型 | `userType` | `u.sUserType` |
  34 +| 作废 | `disabled` | `u.bIsDisabled` |
  35 +| 登录日期 | `lastLoginDate` | `u.tLastLoginDate` |
  36 +| 制单人 | `creator` | `u.sCreatorUsername` |
  37 +
  38 +## 输出 / 结果
  39 +HTTP 200,`Result<PageVO<UserListItemVO>>`
  40 +
  41 +`PageVO<T>` 结构(通用分页包装):
  42 +```json
  43 +{
  44 + "total": 25,
  45 + "page": 1,
  46 + "pageSize": 20,
  47 + "list": [ ...UserListItemVO... ]
  48 +}
  49 +```
  50 +
  51 +`UserListItemVO` 字段(禁止返回 sPasswordHash / iLoginFailCount / tLockUntil):
  52 +
  53 +| JSON 字段 | 来源列 | 可 null |
  54 +|---|---|---|
  55 +| sId | usr_user.sId | 否 |
  56 +| sUsername | usr_user.sUsername | 否 |
  57 +| sUserCode | usr_user.sUserCode | 否 |
  58 +| sUserType | usr_user.sUserType | 否 |
  59 +| sLanguage | usr_user.sLanguage | 否 |
  60 +| bIsDisabled | usr_user.bIsDisabled | 否 |
  61 +| tLastLoginDate | usr_user.tLastLoginDate | 是 |
  62 +| sCreatorUsername | usr_user.sCreatorUsername | 是 |
  63 +| tCreateDate | usr_user.tCreateDate | 否 |
  64 +| sStaffName | tStaff.sStaffName | 是(LEFT JOIN) |
  65 +| sDepartment | tStaff.sDepartment | 是(LEFT JOIN) |
  66 +
  67 +## 业务规则
  68 +
  69 +1. **多租户隔离**:所有查询必须附加 `u.sBrandsId = principal.brandId()`
  70 +2. **queryValue 为空**:忽略查询条件,返回该品牌全部用户
  71 +3. **匹配方式映射**(文本字段:username / staffName / userCode / department / userType / creator):
  72 + - `contains` → `LIKE '%value%'`
  73 + - `notContains` → `NOT LIKE '%value%'`
  74 + - `equals` → `= 'value'`
  75 +4. **布尔字段(disabled)**:仅有意义的匹配是 equals;queryValue="是" → `bIsDisabled = 1`;"否" → `bIsDisabled = 0`;其他值(包括 contains/notContains 的非 "是"/"否")忽略条件
  76 +5. **日期字段(lastLoginDate)**:queryValue 格式 `YYYY-MM-DD`;无论 matchType 为何,均使用 `DATE(tLastLoginDate) = 'YYYY-MM-DD'`
  77 +6. **pageSize 截断**:pageSize > 100 时强制截断为 100
  78 +7. **分页越界**:page 超出总页数时返回最后一页(MyBatis-Plus `Page` 默认行为)
  79 +8. **职员 JOIN**:`LEFT JOIN tStaff s ON u.sEmployeeId = s.sId AND s.sBrandsId = u.sBrandsId AND s.bDeleted = 0`;未关联职员时 sStaffName / sDepartment 为 null
  80 +
  81 +## 边界与约束
  82 +- 接口为只读,无写副作用
  83 +- 鉴权失败返回 401(SecurityConfig.authenticationEntryPoint 已配置)
  84 +- 单次最多返回 100 条
  85 +
  86 +## 依赖的 schema 表 / 字段
  87 +- `usr_user (别名 u)`:sId, sUsername, sUserCode, sUserType, sLanguage, bIsDisabled, sEmployeeId, sCreatorUsername, tLastLoginDate, tCreateDate, sBrandsId
  88 +- `tStaff (别名 s)`:sId, sStaffName, sDepartment, sBrandsId, bDeleted
  89 +
  90 +## 依赖的接口
  91 +- `POST /api/auth/login`(REQ-USR-004,获取 Bearer Token 供本接口鉴权使用)
  92 +
  93 +## 验收标准
  94 +1. `GET /api/usr/users`(无参)HTTP 200,返回本品牌所有用户,不含密码字段
  95 +2. `queryField=username&matchType=contains&queryValue=admin` 仅返回 sUsername 含 "admin" 的用户
  96 +3. `queryField=disabled&matchType=equals&queryValue=是` 仅返回 bIsDisabled=1 的记录
  97 +4. 无匹配条件时返回 `{ total: 0, list: [] }` 而非 4xx/5xx
  98 +5. page=999(超出总页)返回最后一页而不报错
  99 +6. Token 品牌 A 无法查到品牌 B 的用户(多租户隔离)
  100 +7. 响应 JSON 中不含 sPasswordHash / iLoginFailCount / tLockUntil
  101 +8. 无 Token 请求返回 401
... ...
docs/superpowers/specs/2026-05-08-REQ-USR-004.md 0 → 100644
  1 +---
  2 +req_id: REQ-USR-004
  3 +date: 2026-05-08
  4 +module: module_usr
  5 +---
  6 +
  7 +# Spec: REQ-USR-004 — 用户登录
  8 +
  9 +## 目标
  10 +
  11 +用户在登录页选择所属公司(brand)、输入用户名和密码,完成身份认证后获取 JWT Access Token 及 Refresh Token,用于后续所有接口鉴权。多租户隔离:每次登录的用户查询限定在所选 brand 的 sBrandsId 范围内。
  12 +
  13 +## 输入 / 触发
  14 +
  15 +**POST /api/auth/login**(公开接口,无需 Bearer Token)
  16 +
  17 +```json
  18 +{
  19 + "brandNo": "string", // brand.sNo,前端版本下拉选中项
  20 + "username": "string", // usr_user.sUsername
  21 + "password": "string" // 明文密码(HTTPS 传输),后端用 BCrypt 校验
  22 +}
  23 +```
  24 +
  25 +**辅助:版本下拉数据**
  26 +前端需预先调用 `GET /api/auth/brands` 获取 brand 列表(sNo / sName),填充版本下拉框。默认选中 `sName = "标准版"` 对应的项(若无则选第一项)。此接口亦无需鉴权。
  27 +
  28 +**POST /api/auth/refresh**(无需 Bearer Token,需 Refresh Token)
  29 +
  30 +```json
  31 +{ "refreshToken": "string" }
  32 +```
  33 +
  34 +## 输出 / 结果
  35 +
  36 +**登录成功(HTTP 200)**
  37 +
  38 +```json
  39 +{
  40 + "code": 200,
  41 + "message": "登录成功",
  42 + "data": {
  43 + "accessToken": "<jwt>",
  44 + "refreshToken": "<jwt>",
  45 + "expiresIn": 86400,
  46 + "userInfo": {
  47 + "userId": "string", // usr_user.sId
  48 + "username": "string", // usr_user.sUsername
  49 + "userType": "string", // 普通用户 | 超级管理员
  50 + "language": "string", // 中文 | 英文 | 繁体
  51 + "brandId": "string" // brand.sId,写入 token claims 用于后续多租户隔离
  52 + }
  53 + }
  54 +}
  55 +```
  56 +
  57 +**失败(业务错误,HTTP 200 + code ≠ 0)**
  58 +
  59 +| code | message | 场景 |
  60 +|---|---|---|
  61 +| 40100 | 用户名或密码错误 | 密码错误 / 用户不存在 / brand 不存在(统一文案,防枚举) |
  62 +| 40101 | 账号已被禁用,请联系管理员 | bIsDisabled = 1 |
  63 +| 40102 | 账号已被锁定,请 {N} 分钟后重试 | tLockUntil > NOW() |
  64 +| 40103 | Refresh Token 已失效,请重新登录 | refresh 接口 token 过期或篡改 |
  65 +
  66 +## 业务规则
  67 +
  68 +1. **多租户查询**:先 `SELECT * FROM brand WHERE sNo = ?` 得 `brandId = brand.sId`;再 `SELECT * FROM usr_user WHERE sUsername = ? AND sBrandsId = ?`(brandId)。brand 不存在或用户不存在统一返回 40100(防枚举)。
  69 +
  70 +2. **密码校验**:`BCryptPasswordEncoder.matches(plainPassword, usr_user.sPasswordHash)`。
  71 +
  72 +3. **禁用检查**:`bIsDisabled = 1` → 返回 40101,不更新失败计数。
  73 +
  74 +4. **锁定检查**:`tLockUntil IS NOT NULL AND tLockUntil > NOW()` → 返回 40102,剩余分钟数 = `CEIL((tLockUntil - NOW()) / 60)`。
  75 +
  76 +5. **失败计数**:密码错误时 `iLoginFailCount += 1`;达到 5 次时设 `tLockUntil = NOW() + 30 分钟`,同时返回 40102。
  77 +
  78 +6. **登录成功**:
  79 + - 重置 `iLoginFailCount = 0`,`tLockUntil = NULL`
  80 + - 更新 `tLastLoginDate = NOW()`
  81 + - 签发 Access Token(24h)+ Refresh Token(7d),均为 JWT,签名密钥来自 `JWT_SECRET`(从 `.env.local` 注入,`application.yml` 中 `${JWT_SECRET}`)
  82 +
  83 +7. **JWT Claims**:
  84 + - Access Token:`sub = usr_user.sId`,`username`,`userType`,`brandId = brand.sId`,`exp = now + 24h`
  85 + - Refresh Token:`sub = usr_user.sId`,`brandId`,`type = refresh`,`exp = now + 7d`
  86 +
  87 +8. **Token 刷新**(POST /api/auth/refresh):校验 Refresh Token 签名与 `type=refresh`;从 claims 读 `sub`(userId)和 `brandId` 重新查库确认用户仍有效;签发新 Access Token(24h);Refresh Token **不续期**(滑动窗口留给后续迭代)。
  88 +
  89 +9. **版本下拉**(GET /api/auth/brands):`SELECT sNo, sName FROM brand ORDER BY sName`;无需分页(品牌数量通常极少)。
  90 +
  91 +## 边界与约束
  92 +
  93 +- 所有接口均走 HTTPS(Nginx 层保障,后端不强制)
  94 +- 登录日志(IP + 时间戳)通过 Spring Security 的 `AuthenticationSuccessHandler` / `AuthenticationFailureHandler` 写 Logback,不写业务表
  95 +- Refresh Token 无服务端存储(无状态 JWT);强制退出由后续迭代(引入 token 黑名单)实现;本期用 7 天过期控制
  96 +- 密码明文绝不落库,`sPasswordHash` 仅存 BCrypt 60 字符哈希
  97 +- Access Token 不含密码 hash,不含任何敏感字段
  98 +- 防暴力破解:5 次失败锁 30 分钟(数据库层,不依赖内存,重启后有效)
  99 +- `GET /api/auth/brands` 结果可加短缓存(30s),减少登录页加载压力
  100 +
  101 +## 依赖的 schema 表 / 字段
  102 +
  103 +**usr_user**(主要读写字段)
  104 +
  105 +| 字段 | 操作 | 说明 |
  106 +|---|---|---|
  107 +| `sUsername` | READ | 登录名匹配 |
  108 +| `sBrandsId` | READ | 多租户范围限定 |
  109 +| `sPasswordHash` | READ | BCrypt 校验 |
  110 +| `bIsDisabled` | READ | 禁用检查 |
  111 +| `tLockUntil` | READ / WRITE | 锁定截止时间 |
  112 +| `iLoginFailCount` | READ / WRITE | 连续失败次数 |
  113 +| `tLastLoginDate` | WRITE | 成功后更新 |
  114 +| `sId` | READ | 写入 JWT sub |
  115 +| `sUserType` | READ | 写入 JWT claims |
  116 +| `sLanguage` | READ | 返回 userInfo |
  117 +
  118 +**brand**
  119 +
  120 +| 字段 | 操作 | 说明 |
  121 +|---|---|---|
  122 +| `sNo` | READ | 版本下拉匹配键 |
  123 +| `sId` | READ | 写入 JWT brandId claim |
  124 +| `sName` | READ | 版本下拉显示名 |
  125 +
  126 +## 依赖的接口
  127 +
  128 +- 自身即认证入口,无上游接口依赖
  129 +- 对外暴露:`POST /api/auth/login`、`POST /api/auth/refresh`、`GET /api/auth/brands`(均无鉴权)
  130 +
  131 +## 验收标准
  132 +
  133 +1. 正确 brandNo + username + password → HTTP 200,返回有效 accessToken,`POST /api/usr/users` 携带该 token 鉴权通过
  134 +2. 错误密码 → code=40100,且不暴露是用户名还是密码错了
  135 +3. 连续 5 次错误密码 → 第 5 次返回 code=40102(含剩余分钟数),后续在锁定期内再请求仍返回 40102
  136 +4. 锁定 30 分钟后自动解锁,正确凭据可再次登录
  137 +5. `bIsDisabled = 1` 的账号登录 → code=40101
  138 +6. brand.sNo 不存在 → code=40100(与密码错误同一文案)
  139 +7. 用同一账号、不同 brandId 登录 → 互不干扰(多租户隔离)
  140 +8. 有效 refreshToken → POST /api/auth/refresh 返回新 accessToken
  141 +9. 过期 / 伪造 refreshToken → code=40103
  142 +10. GET /api/auth/brands 返回所有 brand 列表(sNo + sName)
... ...
frontend/index.html 0 → 100644
  1 +<!doctype html>
  2 +<html lang="zh-CN">
  3 + <head>
  4 + <meta charset="UTF-8" />
  5 + <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  7 + <title>小羚羊 ERP</title>
  8 + </head>
  9 + <body>
  10 + <div id="root"></div>
  11 + <script type="module" src="/src/main.tsx"></script>
  12 + </body>
  13 +</html>
... ...
frontend/package-lock.json 0 → 100644
  1 +{
  2 + "name": "erp-frontend",
  3 + "version": "0.0.1",
  4 + "lockfileVersion": 3,
  5 + "requires": true,
  6 + "packages": {
  7 + "": {
  8 + "name": "erp-frontend",
  9 + "version": "0.0.1",
  10 + "dependencies": {
  11 + "@ant-design/icons": "^5.3.7",
  12 + "@reduxjs/toolkit": "^2.2.5",
  13 + "antd": "^5.18.0",
  14 + "axios": "^1.7.2",
  15 + "dayjs": "^1.11.11",
  16 + "react": "^18.3.1",
  17 + "react-dom": "^18.3.1",
  18 + "react-redux": "^9.1.2",
  19 + "react-router-dom": "^6.23.1"
  20 + },
  21 + "devDependencies": {
  22 + "@testing-library/jest-dom": "^6.4.6",
  23 + "@testing-library/react": "^16.0.0",
  24 + "@testing-library/user-event": "^14.5.2",
  25 + "@types/react": "^18.3.3",
  26 + "@types/react-dom": "^18.3.0",
  27 + "@vitejs/plugin-react": "^4.3.1",
  28 + "jsdom": "^24.1.0",
  29 + "typescript": "^5.4.5",
  30 + "vite": "^5.3.1",
  31 + "vitest": "^1.6.0"
  32 + }
  33 + },
  34 + "node_modules/@adobe/css-tools": {
  35 + "version": "4.4.4",
  36 + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
  37 + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
  38 + "dev": true,
  39 + "license": "MIT"
  40 + },
  41 + "node_modules/@ant-design/colors": {
  42 + "version": "7.2.1",
  43 + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz",
  44 + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==",
  45 + "license": "MIT",
  46 + "dependencies": {
  47 + "@ant-design/fast-color": "^2.0.6"
  48 + }
  49 + },
  50 + "node_modules/@ant-design/cssinjs": {
  51 + "version": "1.24.0",
  52 + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz",
  53 + "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==",
  54 + "license": "MIT",
  55 + "dependencies": {
  56 + "@babel/runtime": "^7.11.1",
  57 + "@emotion/hash": "^0.8.0",
  58 + "@emotion/unitless": "^0.7.5",
  59 + "classnames": "^2.3.1",
  60 + "csstype": "^3.1.3",
  61 + "rc-util": "^5.35.0",
  62 + "stylis": "^4.3.4"
  63 + },
  64 + "peerDependencies": {
  65 + "react": ">=16.0.0",
  66 + "react-dom": ">=16.0.0"
  67 + }
  68 + },
  69 + "node_modules/@ant-design/cssinjs-utils": {
  70 + "version": "1.1.3",
  71 + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz",
  72 + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==",
  73 + "license": "MIT",
  74 + "dependencies": {
  75 + "@ant-design/cssinjs": "^1.21.0",
  76 + "@babel/runtime": "^7.23.2",
  77 + "rc-util": "^5.38.0"
  78 + },
  79 + "peerDependencies": {
  80 + "react": ">=16.9.0",
  81 + "react-dom": ">=16.9.0"
  82 + }
  83 + },
  84 + "node_modules/@ant-design/fast-color": {
  85 + "version": "2.0.6",
  86 + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz",
  87 + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==",
  88 + "license": "MIT",
  89 + "dependencies": {
  90 + "@babel/runtime": "^7.24.7"
  91 + },
  92 + "engines": {
  93 + "node": ">=8.x"
  94 + }
  95 + },
  96 + "node_modules/@ant-design/icons": {
  97 + "version": "5.6.1",
  98 + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz",
  99 + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
  100 + "license": "MIT",
  101 + "dependencies": {
  102 + "@ant-design/colors": "^7.0.0",
  103 + "@ant-design/icons-svg": "^4.4.0",
  104 + "@babel/runtime": "^7.24.8",
  105 + "classnames": "^2.2.6",
  106 + "rc-util": "^5.31.1"
  107 + },
  108 + "engines": {
  109 + "node": ">=8"
  110 + },
  111 + "peerDependencies": {
  112 + "react": ">=16.0.0",
  113 + "react-dom": ">=16.0.0"
  114 + }
  115 + },
  116 + "node_modules/@ant-design/icons-svg": {
  117 + "version": "4.4.2",
  118 + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
  119 + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
  120 + "license": "MIT"
  121 + },
  122 + "node_modules/@ant-design/react-slick": {
  123 + "version": "1.1.2",
  124 + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz",
  125 + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==",
  126 + "license": "MIT",
  127 + "dependencies": {
  128 + "@babel/runtime": "^7.10.4",
  129 + "classnames": "^2.2.5",
  130 + "json2mq": "^0.2.0",
  131 + "resize-observer-polyfill": "^1.5.1",
  132 + "throttle-debounce": "^5.0.0"
  133 + },
  134 + "peerDependencies": {
  135 + "react": ">=16.9.0"
  136 + }
  137 + },
  138 + "node_modules/@asamuzakjp/css-color": {
  139 + "version": "3.2.0",
  140 + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
  141 + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
  142 + "dev": true,
  143 + "license": "MIT",
  144 + "dependencies": {
  145 + "@csstools/css-calc": "^2.1.3",
  146 + "@csstools/css-color-parser": "^3.0.9",
  147 + "@csstools/css-parser-algorithms": "^3.0.4",
  148 + "@csstools/css-tokenizer": "^3.0.3",
  149 + "lru-cache": "^10.4.3"
  150 + }
  151 + },
  152 + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
  153 + "version": "10.4.3",
  154 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
  155 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
  156 + "dev": true,
  157 + "license": "ISC"
  158 + },
  159 + "node_modules/@babel/code-frame": {
  160 + "version": "7.29.0",
  161 + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
  162 + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
  163 + "dev": true,
  164 + "license": "MIT",
  165 + "dependencies": {
  166 + "@babel/helper-validator-identifier": "^7.28.5",
  167 + "js-tokens": "^4.0.0",
  168 + "picocolors": "^1.1.1"
  169 + },
  170 + "engines": {
  171 + "node": ">=6.9.0"
  172 + }
  173 + },
  174 + "node_modules/@babel/compat-data": {
  175 + "version": "7.29.3",
  176 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz",
  177 + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==",
  178 + "dev": true,
  179 + "license": "MIT",
  180 + "engines": {
  181 + "node": ">=6.9.0"
  182 + }
  183 + },
  184 + "node_modules/@babel/core": {
  185 + "version": "7.29.0",
  186 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
  187 + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
  188 + "dev": true,
  189 + "license": "MIT",
  190 + "dependencies": {
  191 + "@babel/code-frame": "^7.29.0",
  192 + "@babel/generator": "^7.29.0",
  193 + "@babel/helper-compilation-targets": "^7.28.6",
  194 + "@babel/helper-module-transforms": "^7.28.6",
  195 + "@babel/helpers": "^7.28.6",
  196 + "@babel/parser": "^7.29.0",
  197 + "@babel/template": "^7.28.6",
  198 + "@babel/traverse": "^7.29.0",
  199 + "@babel/types": "^7.29.0",
  200 + "@jridgewell/remapping": "^2.3.5",
  201 + "convert-source-map": "^2.0.0",
  202 + "debug": "^4.1.0",
  203 + "gensync": "^1.0.0-beta.2",
  204 + "json5": "^2.2.3",
  205 + "semver": "^6.3.1"
  206 + },
  207 + "engines": {
  208 + "node": ">=6.9.0"
  209 + },
  210 + "funding": {
  211 + "type": "opencollective",
  212 + "url": "https://opencollective.com/babel"
  213 + }
  214 + },
  215 + "node_modules/@babel/generator": {
  216 + "version": "7.29.1",
  217 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
  218 + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
  219 + "dev": true,
  220 + "license": "MIT",
  221 + "dependencies": {
  222 + "@babel/parser": "^7.29.0",
  223 + "@babel/types": "^7.29.0",
  224 + "@jridgewell/gen-mapping": "^0.3.12",
  225 + "@jridgewell/trace-mapping": "^0.3.28",
  226 + "jsesc": "^3.0.2"
  227 + },
  228 + "engines": {
  229 + "node": ">=6.9.0"
  230 + }
  231 + },
  232 + "node_modules/@babel/helper-compilation-targets": {
  233 + "version": "7.28.6",
  234 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
  235 + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
  236 + "dev": true,
  237 + "license": "MIT",
  238 + "dependencies": {
  239 + "@babel/compat-data": "^7.28.6",
  240 + "@babel/helper-validator-option": "^7.27.1",
  241 + "browserslist": "^4.24.0",
  242 + "lru-cache": "^5.1.1",
  243 + "semver": "^6.3.1"
  244 + },
  245 + "engines": {
  246 + "node": ">=6.9.0"
  247 + }
  248 + },
  249 + "node_modules/@babel/helper-globals": {
  250 + "version": "7.28.0",
  251 + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
  252 + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
  253 + "dev": true,
  254 + "license": "MIT",
  255 + "engines": {
  256 + "node": ">=6.9.0"
  257 + }
  258 + },
  259 + "node_modules/@babel/helper-module-imports": {
  260 + "version": "7.28.6",
  261 + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
  262 + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
  263 + "dev": true,
  264 + "license": "MIT",
  265 + "dependencies": {
  266 + "@babel/traverse": "^7.28.6",
  267 + "@babel/types": "^7.28.6"
  268 + },
  269 + "engines": {
  270 + "node": ">=6.9.0"
  271 + }
  272 + },
  273 + "node_modules/@babel/helper-module-transforms": {
  274 + "version": "7.28.6",
  275 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
  276 + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
  277 + "dev": true,
  278 + "license": "MIT",
  279 + "dependencies": {
  280 + "@babel/helper-module-imports": "^7.28.6",
  281 + "@babel/helper-validator-identifier": "^7.28.5",
  282 + "@babel/traverse": "^7.28.6"
  283 + },
  284 + "engines": {
  285 + "node": ">=6.9.0"
  286 + },
  287 + "peerDependencies": {
  288 + "@babel/core": "^7.0.0"
  289 + }
  290 + },
  291 + "node_modules/@babel/helper-plugin-utils": {
  292 + "version": "7.28.6",
  293 + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
  294 + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
  295 + "dev": true,
  296 + "license": "MIT",
  297 + "engines": {
  298 + "node": ">=6.9.0"
  299 + }
  300 + },
  301 + "node_modules/@babel/helper-string-parser": {
  302 + "version": "7.27.1",
  303 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
  304 + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
  305 + "dev": true,
  306 + "license": "MIT",
  307 + "engines": {
  308 + "node": ">=6.9.0"
  309 + }
  310 + },
  311 + "node_modules/@babel/helper-validator-identifier": {
  312 + "version": "7.28.5",
  313 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
  314 + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
  315 + "dev": true,
  316 + "license": "MIT",
  317 + "engines": {
  318 + "node": ">=6.9.0"
  319 + }
  320 + },
  321 + "node_modules/@babel/helper-validator-option": {
  322 + "version": "7.27.1",
  323 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
  324 + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
  325 + "dev": true,
  326 + "license": "MIT",
  327 + "engines": {
  328 + "node": ">=6.9.0"
  329 + }
  330 + },
  331 + "node_modules/@babel/helpers": {
  332 + "version": "7.29.2",
  333 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
  334 + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
  335 + "dev": true,
  336 + "license": "MIT",
  337 + "dependencies": {
  338 + "@babel/template": "^7.28.6",
  339 + "@babel/types": "^7.29.0"
  340 + },
  341 + "engines": {
  342 + "node": ">=6.9.0"
  343 + }
  344 + },
  345 + "node_modules/@babel/parser": {
  346 + "version": "7.29.3",
  347 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
  348 + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
  349 + "dev": true,
  350 + "license": "MIT",
  351 + "dependencies": {
  352 + "@babel/types": "^7.29.0"
  353 + },
  354 + "bin": {
  355 + "parser": "bin/babel-parser.js"
  356 + },
  357 + "engines": {
  358 + "node": ">=6.0.0"
  359 + }
  360 + },
  361 + "node_modules/@babel/plugin-transform-react-jsx-self": {
  362 + "version": "7.27.1",
  363 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
  364 + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
  365 + "dev": true,
  366 + "license": "MIT",
  367 + "dependencies": {
  368 + "@babel/helper-plugin-utils": "^7.27.1"
  369 + },
  370 + "engines": {
  371 + "node": ">=6.9.0"
  372 + },
  373 + "peerDependencies": {
  374 + "@babel/core": "^7.0.0-0"
  375 + }
  376 + },
  377 + "node_modules/@babel/plugin-transform-react-jsx-source": {
  378 + "version": "7.27.1",
  379 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
  380 + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
  381 + "dev": true,
  382 + "license": "MIT",
  383 + "dependencies": {
  384 + "@babel/helper-plugin-utils": "^7.27.1"
  385 + },
  386 + "engines": {
  387 + "node": ">=6.9.0"
  388 + },
  389 + "peerDependencies": {
  390 + "@babel/core": "^7.0.0-0"
  391 + }
  392 + },
  393 + "node_modules/@babel/runtime": {
  394 + "version": "7.29.2",
  395 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
  396 + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
  397 + "license": "MIT",
  398 + "engines": {
  399 + "node": ">=6.9.0"
  400 + }
  401 + },
  402 + "node_modules/@babel/template": {
  403 + "version": "7.28.6",
  404 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
  405 + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
  406 + "dev": true,
  407 + "license": "MIT",
  408 + "dependencies": {
  409 + "@babel/code-frame": "^7.28.6",
  410 + "@babel/parser": "^7.28.6",
  411 + "@babel/types": "^7.28.6"
  412 + },
  413 + "engines": {
  414 + "node": ">=6.9.0"
  415 + }
  416 + },
  417 + "node_modules/@babel/traverse": {
  418 + "version": "7.29.0",
  419 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
  420 + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
  421 + "dev": true,
  422 + "license": "MIT",
  423 + "dependencies": {
  424 + "@babel/code-frame": "^7.29.0",
  425 + "@babel/generator": "^7.29.0",
  426 + "@babel/helper-globals": "^7.28.0",
  427 + "@babel/parser": "^7.29.0",
  428 + "@babel/template": "^7.28.6",
  429 + "@babel/types": "^7.29.0",
  430 + "debug": "^4.3.1"
  431 + },
  432 + "engines": {
  433 + "node": ">=6.9.0"
  434 + }
  435 + },
  436 + "node_modules/@babel/types": {
  437 + "version": "7.29.0",
  438 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
  439 + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
  440 + "dev": true,
  441 + "license": "MIT",
  442 + "dependencies": {
  443 + "@babel/helper-string-parser": "^7.27.1",
  444 + "@babel/helper-validator-identifier": "^7.28.5"
  445 + },
  446 + "engines": {
  447 + "node": ">=6.9.0"
  448 + }
  449 + },
  450 + "node_modules/@csstools/color-helpers": {
  451 + "version": "5.1.0",
  452 + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
  453 + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
  454 + "dev": true,
  455 + "funding": [
  456 + {
  457 + "type": "github",
  458 + "url": "https://github.com/sponsors/csstools"
  459 + },
  460 + {
  461 + "type": "opencollective",
  462 + "url": "https://opencollective.com/csstools"
  463 + }
  464 + ],
  465 + "license": "MIT-0",
  466 + "engines": {
  467 + "node": ">=18"
  468 + }
  469 + },
  470 + "node_modules/@csstools/css-calc": {
  471 + "version": "2.1.4",
  472 + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
  473 + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
  474 + "dev": true,
  475 + "funding": [
  476 + {
  477 + "type": "github",
  478 + "url": "https://github.com/sponsors/csstools"
  479 + },
  480 + {
  481 + "type": "opencollective",
  482 + "url": "https://opencollective.com/csstools"
  483 + }
  484 + ],
  485 + "license": "MIT",
  486 + "engines": {
  487 + "node": ">=18"
  488 + },
  489 + "peerDependencies": {
  490 + "@csstools/css-parser-algorithms": "^3.0.5",
  491 + "@csstools/css-tokenizer": "^3.0.4"
  492 + }
  493 + },
  494 + "node_modules/@csstools/css-color-parser": {
  495 + "version": "3.1.0",
  496 + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
  497 + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
  498 + "dev": true,
  499 + "funding": [
  500 + {
  501 + "type": "github",
  502 + "url": "https://github.com/sponsors/csstools"
  503 + },
  504 + {
  505 + "type": "opencollective",
  506 + "url": "https://opencollective.com/csstools"
  507 + }
  508 + ],
  509 + "license": "MIT",
  510 + "dependencies": {
  511 + "@csstools/color-helpers": "^5.1.0",
  512 + "@csstools/css-calc": "^2.1.4"
  513 + },
  514 + "engines": {
  515 + "node": ">=18"
  516 + },
  517 + "peerDependencies": {
  518 + "@csstools/css-parser-algorithms": "^3.0.5",
  519 + "@csstools/css-tokenizer": "^3.0.4"
  520 + }
  521 + },
  522 + "node_modules/@csstools/css-parser-algorithms": {
  523 + "version": "3.0.5",
  524 + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
  525 + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
  526 + "dev": true,
  527 + "funding": [
  528 + {
  529 + "type": "github",
  530 + "url": "https://github.com/sponsors/csstools"
  531 + },
  532 + {
  533 + "type": "opencollective",
  534 + "url": "https://opencollective.com/csstools"
  535 + }
  536 + ],
  537 + "license": "MIT",
  538 + "engines": {
  539 + "node": ">=18"
  540 + },
  541 + "peerDependencies": {
  542 + "@csstools/css-tokenizer": "^3.0.4"
  543 + }
  544 + },
  545 + "node_modules/@csstools/css-tokenizer": {
  546 + "version": "3.0.4",
  547 + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
  548 + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
  549 + "dev": true,
  550 + "funding": [
  551 + {
  552 + "type": "github",
  553 + "url": "https://github.com/sponsors/csstools"
  554 + },
  555 + {
  556 + "type": "opencollective",
  557 + "url": "https://opencollective.com/csstools"
  558 + }
  559 + ],
  560 + "license": "MIT",
  561 + "engines": {
  562 + "node": ">=18"
  563 + }
  564 + },
  565 + "node_modules/@emotion/hash": {
  566 + "version": "0.8.0",
  567 + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
  568 + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
  569 + "license": "MIT"
  570 + },
  571 + "node_modules/@emotion/unitless": {
  572 + "version": "0.7.5",
  573 + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
  574 + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
  575 + "license": "MIT"
  576 + },
  577 + "node_modules/@esbuild/aix-ppc64": {
  578 + "version": "0.21.5",
  579 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
  580 + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
  581 + "cpu": [
  582 + "ppc64"
  583 + ],
  584 + "dev": true,
  585 + "license": "MIT",
  586 + "optional": true,
  587 + "os": [
  588 + "aix"
  589 + ],
  590 + "engines": {
  591 + "node": ">=12"
  592 + }
  593 + },
  594 + "node_modules/@esbuild/android-arm": {
  595 + "version": "0.21.5",
  596 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
  597 + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
  598 + "cpu": [
  599 + "arm"
  600 + ],
  601 + "dev": true,
  602 + "license": "MIT",
  603 + "optional": true,
  604 + "os": [
  605 + "android"
  606 + ],
  607 + "engines": {
  608 + "node": ">=12"
  609 + }
  610 + },
  611 + "node_modules/@esbuild/android-arm64": {
  612 + "version": "0.21.5",
  613 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
  614 + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
  615 + "cpu": [
  616 + "arm64"
  617 + ],
  618 + "dev": true,
  619 + "license": "MIT",
  620 + "optional": true,
  621 + "os": [
  622 + "android"
  623 + ],
  624 + "engines": {
  625 + "node": ">=12"
  626 + }
  627 + },
  628 + "node_modules/@esbuild/android-x64": {
  629 + "version": "0.21.5",
  630 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
  631 + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
  632 + "cpu": [
  633 + "x64"
  634 + ],
  635 + "dev": true,
  636 + "license": "MIT",
  637 + "optional": true,
  638 + "os": [
  639 + "android"
  640 + ],
  641 + "engines": {
  642 + "node": ">=12"
  643 + }
  644 + },
  645 + "node_modules/@esbuild/darwin-arm64": {
  646 + "version": "0.21.5",
  647 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
  648 + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
  649 + "cpu": [
  650 + "arm64"
  651 + ],
  652 + "dev": true,
  653 + "license": "MIT",
  654 + "optional": true,
  655 + "os": [
  656 + "darwin"
  657 + ],
  658 + "engines": {
  659 + "node": ">=12"
  660 + }
  661 + },
  662 + "node_modules/@esbuild/darwin-x64": {
  663 + "version": "0.21.5",
  664 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
  665 + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
  666 + "cpu": [
  667 + "x64"
  668 + ],
  669 + "dev": true,
  670 + "license": "MIT",
  671 + "optional": true,
  672 + "os": [
  673 + "darwin"
  674 + ],
  675 + "engines": {
  676 + "node": ">=12"
  677 + }
  678 + },
  679 + "node_modules/@esbuild/freebsd-arm64": {
  680 + "version": "0.21.5",
  681 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
  682 + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
  683 + "cpu": [
  684 + "arm64"
  685 + ],
  686 + "dev": true,
  687 + "license": "MIT",
  688 + "optional": true,
  689 + "os": [
  690 + "freebsd"
  691 + ],
  692 + "engines": {
  693 + "node": ">=12"
  694 + }
  695 + },
  696 + "node_modules/@esbuild/freebsd-x64": {
  697 + "version": "0.21.5",
  698 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
  699 + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
  700 + "cpu": [
  701 + "x64"
  702 + ],
  703 + "dev": true,
  704 + "license": "MIT",
  705 + "optional": true,
  706 + "os": [
  707 + "freebsd"
  708 + ],
  709 + "engines": {
  710 + "node": ">=12"
  711 + }
  712 + },
  713 + "node_modules/@esbuild/linux-arm": {
  714 + "version": "0.21.5",
  715 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
  716 + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
  717 + "cpu": [
  718 + "arm"
  719 + ],
  720 + "dev": true,
  721 + "license": "MIT",
  722 + "optional": true,
  723 + "os": [
  724 + "linux"
  725 + ],
  726 + "engines": {
  727 + "node": ">=12"
  728 + }
  729 + },
  730 + "node_modules/@esbuild/linux-arm64": {
  731 + "version": "0.21.5",
  732 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
  733 + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
  734 + "cpu": [
  735 + "arm64"
  736 + ],
  737 + "dev": true,
  738 + "license": "MIT",
  739 + "optional": true,
  740 + "os": [
  741 + "linux"
  742 + ],
  743 + "engines": {
  744 + "node": ">=12"
  745 + }
  746 + },
  747 + "node_modules/@esbuild/linux-ia32": {
  748 + "version": "0.21.5",
  749 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
  750 + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
  751 + "cpu": [
  752 + "ia32"
  753 + ],
  754 + "dev": true,
  755 + "license": "MIT",
  756 + "optional": true,
  757 + "os": [
  758 + "linux"
  759 + ],
  760 + "engines": {
  761 + "node": ">=12"
  762 + }
  763 + },
  764 + "node_modules/@esbuild/linux-loong64": {
  765 + "version": "0.21.5",
  766 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
  767 + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
  768 + "cpu": [
  769 + "loong64"
  770 + ],
  771 + "dev": true,
  772 + "license": "MIT",
  773 + "optional": true,
  774 + "os": [
  775 + "linux"
  776 + ],
  777 + "engines": {
  778 + "node": ">=12"
  779 + }
  780 + },
  781 + "node_modules/@esbuild/linux-mips64el": {
  782 + "version": "0.21.5",
  783 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
  784 + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
  785 + "cpu": [
  786 + "mips64el"
  787 + ],
  788 + "dev": true,
  789 + "license": "MIT",
  790 + "optional": true,
  791 + "os": [
  792 + "linux"
  793 + ],
  794 + "engines": {
  795 + "node": ">=12"
  796 + }
  797 + },
  798 + "node_modules/@esbuild/linux-ppc64": {
  799 + "version": "0.21.5",
  800 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
  801 + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
  802 + "cpu": [
  803 + "ppc64"
  804 + ],
  805 + "dev": true,
  806 + "license": "MIT",
  807 + "optional": true,
  808 + "os": [
  809 + "linux"
  810 + ],
  811 + "engines": {
  812 + "node": ">=12"
  813 + }
  814 + },
  815 + "node_modules/@esbuild/linux-riscv64": {
  816 + "version": "0.21.5",
  817 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
  818 + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
  819 + "cpu": [
  820 + "riscv64"
  821 + ],
  822 + "dev": true,
  823 + "license": "MIT",
  824 + "optional": true,
  825 + "os": [
  826 + "linux"
  827 + ],
  828 + "engines": {
  829 + "node": ">=12"
  830 + }
  831 + },
  832 + "node_modules/@esbuild/linux-s390x": {
  833 + "version": "0.21.5",
  834 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
  835 + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
  836 + "cpu": [
  837 + "s390x"
  838 + ],
  839 + "dev": true,
  840 + "license": "MIT",
  841 + "optional": true,
  842 + "os": [
  843 + "linux"
  844 + ],
  845 + "engines": {
  846 + "node": ">=12"
  847 + }
  848 + },
  849 + "node_modules/@esbuild/linux-x64": {
  850 + "version": "0.21.5",
  851 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
  852 + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
  853 + "cpu": [
  854 + "x64"
  855 + ],
  856 + "dev": true,
  857 + "license": "MIT",
  858 + "optional": true,
  859 + "os": [
  860 + "linux"
  861 + ],
  862 + "engines": {
  863 + "node": ">=12"
  864 + }
  865 + },
  866 + "node_modules/@esbuild/netbsd-x64": {
  867 + "version": "0.21.5",
  868 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
  869 + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
  870 + "cpu": [
  871 + "x64"
  872 + ],
  873 + "dev": true,
  874 + "license": "MIT",
  875 + "optional": true,
  876 + "os": [
  877 + "netbsd"
  878 + ],
  879 + "engines": {
  880 + "node": ">=12"
  881 + }
  882 + },
  883 + "node_modules/@esbuild/openbsd-x64": {
  884 + "version": "0.21.5",
  885 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
  886 + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
  887 + "cpu": [
  888 + "x64"
  889 + ],
  890 + "dev": true,
  891 + "license": "MIT",
  892 + "optional": true,
  893 + "os": [
  894 + "openbsd"
  895 + ],
  896 + "engines": {
  897 + "node": ">=12"
  898 + }
  899 + },
  900 + "node_modules/@esbuild/sunos-x64": {
  901 + "version": "0.21.5",
  902 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
  903 + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
  904 + "cpu": [
  905 + "x64"
  906 + ],
  907 + "dev": true,
  908 + "license": "MIT",
  909 + "optional": true,
  910 + "os": [
  911 + "sunos"
  912 + ],
  913 + "engines": {
  914 + "node": ">=12"
  915 + }
  916 + },
  917 + "node_modules/@esbuild/win32-arm64": {
  918 + "version": "0.21.5",
  919 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
  920 + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
  921 + "cpu": [
  922 + "arm64"
  923 + ],
  924 + "dev": true,
  925 + "license": "MIT",
  926 + "optional": true,
  927 + "os": [
  928 + "win32"
  929 + ],
  930 + "engines": {
  931 + "node": ">=12"
  932 + }
  933 + },
  934 + "node_modules/@esbuild/win32-ia32": {
  935 + "version": "0.21.5",
  936 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
  937 + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
  938 + "cpu": [
  939 + "ia32"
  940 + ],
  941 + "dev": true,
  942 + "license": "MIT",
  943 + "optional": true,
  944 + "os": [
  945 + "win32"
  946 + ],
  947 + "engines": {
  948 + "node": ">=12"
  949 + }
  950 + },
  951 + "node_modules/@esbuild/win32-x64": {
  952 + "version": "0.21.5",
  953 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
  954 + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
  955 + "cpu": [
  956 + "x64"
  957 + ],
  958 + "dev": true,
  959 + "license": "MIT",
  960 + "optional": true,
  961 + "os": [
  962 + "win32"
  963 + ],
  964 + "engines": {
  965 + "node": ">=12"
  966 + }
  967 + },
  968 + "node_modules/@jest/schemas": {
  969 + "version": "29.6.3",
  970 + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
  971 + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
  972 + "dev": true,
  973 + "license": "MIT",
  974 + "dependencies": {
  975 + "@sinclair/typebox": "^0.27.8"
  976 + },
  977 + "engines": {
  978 + "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
  979 + }
  980 + },
  981 + "node_modules/@jridgewell/gen-mapping": {
  982 + "version": "0.3.13",
  983 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
  984 + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
  985 + "dev": true,
  986 + "license": "MIT",
  987 + "dependencies": {
  988 + "@jridgewell/sourcemap-codec": "^1.5.0",
  989 + "@jridgewell/trace-mapping": "^0.3.24"
  990 + }
  991 + },
  992 + "node_modules/@jridgewell/remapping": {
  993 + "version": "2.3.5",
  994 + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
  995 + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
  996 + "dev": true,
  997 + "license": "MIT",
  998 + "dependencies": {
  999 + "@jridgewell/gen-mapping": "^0.3.5",
  1000 + "@jridgewell/trace-mapping": "^0.3.24"
  1001 + }
  1002 + },
  1003 + "node_modules/@jridgewell/resolve-uri": {
  1004 + "version": "3.1.2",
  1005 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
  1006 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
  1007 + "dev": true,
  1008 + "license": "MIT",
  1009 + "engines": {
  1010 + "node": ">=6.0.0"
  1011 + }
  1012 + },
  1013 + "node_modules/@jridgewell/sourcemap-codec": {
  1014 + "version": "1.5.5",
  1015 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
  1016 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
  1017 + "dev": true,
  1018 + "license": "MIT"
  1019 + },
  1020 + "node_modules/@jridgewell/trace-mapping": {
  1021 + "version": "0.3.31",
  1022 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
  1023 + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
  1024 + "dev": true,
  1025 + "license": "MIT",
  1026 + "dependencies": {
  1027 + "@jridgewell/resolve-uri": "^3.1.0",
  1028 + "@jridgewell/sourcemap-codec": "^1.4.14"
  1029 + }
  1030 + },
  1031 + "node_modules/@rc-component/async-validator": {
  1032 + "version": "5.1.0",
  1033 + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz",
  1034 + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==",
  1035 + "license": "MIT",
  1036 + "dependencies": {
  1037 + "@babel/runtime": "^7.24.4"
  1038 + },
  1039 + "engines": {
  1040 + "node": ">=14.x"
  1041 + }
  1042 + },
  1043 + "node_modules/@rc-component/color-picker": {
  1044 + "version": "2.0.1",
  1045 + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz",
  1046 + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==",
  1047 + "license": "MIT",
  1048 + "dependencies": {
  1049 + "@ant-design/fast-color": "^2.0.6",
  1050 + "@babel/runtime": "^7.23.6",
  1051 + "classnames": "^2.2.6",
  1052 + "rc-util": "^5.38.1"
  1053 + },
  1054 + "peerDependencies": {
  1055 + "react": ">=16.9.0",
  1056 + "react-dom": ">=16.9.0"
  1057 + }
  1058 + },
  1059 + "node_modules/@rc-component/context": {
  1060 + "version": "1.4.0",
  1061 + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz",
  1062 + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==",
  1063 + "license": "MIT",
  1064 + "dependencies": {
  1065 + "@babel/runtime": "^7.10.1",
  1066 + "rc-util": "^5.27.0"
  1067 + },
  1068 + "peerDependencies": {
  1069 + "react": ">=16.9.0",
  1070 + "react-dom": ">=16.9.0"
  1071 + }
  1072 + },
  1073 + "node_modules/@rc-component/mini-decimal": {
  1074 + "version": "1.1.3",
  1075 + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz",
  1076 + "integrity": "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==",
  1077 + "license": "MIT",
  1078 + "dependencies": {
  1079 + "@babel/runtime": "^7.18.0"
  1080 + },
  1081 + "engines": {
  1082 + "node": ">=8.x"
  1083 + }
  1084 + },
  1085 + "node_modules/@rc-component/mutate-observer": {
  1086 + "version": "1.1.0",
  1087 + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz",
  1088 + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==",
  1089 + "license": "MIT",
  1090 + "dependencies": {
  1091 + "@babel/runtime": "^7.18.0",
  1092 + "classnames": "^2.3.2",
  1093 + "rc-util": "^5.24.4"
  1094 + },
  1095 + "engines": {
  1096 + "node": ">=8.x"
  1097 + },
  1098 + "peerDependencies": {
  1099 + "react": ">=16.9.0",
  1100 + "react-dom": ">=16.9.0"
  1101 + }
  1102 + },
  1103 + "node_modules/@rc-component/portal": {
  1104 + "version": "1.1.2",
  1105 + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz",
  1106 + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==",
  1107 + "license": "MIT",
  1108 + "dependencies": {
  1109 + "@babel/runtime": "^7.18.0",
  1110 + "classnames": "^2.3.2",
  1111 + "rc-util": "^5.24.4"
  1112 + },
  1113 + "engines": {
  1114 + "node": ">=8.x"
  1115 + },
  1116 + "peerDependencies": {
  1117 + "react": ">=16.9.0",
  1118 + "react-dom": ">=16.9.0"
  1119 + }
  1120 + },
  1121 + "node_modules/@rc-component/qrcode": {
  1122 + "version": "1.1.1",
  1123 + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz",
  1124 + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==",
  1125 + "license": "MIT",
  1126 + "dependencies": {
  1127 + "@babel/runtime": "^7.24.7"
  1128 + },
  1129 + "engines": {
  1130 + "node": ">=8.x"
  1131 + },
  1132 + "peerDependencies": {
  1133 + "react": ">=16.9.0",
  1134 + "react-dom": ">=16.9.0"
  1135 + }
  1136 + },
  1137 + "node_modules/@rc-component/tour": {
  1138 + "version": "1.15.1",
  1139 + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz",
  1140 + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==",
  1141 + "license": "MIT",
  1142 + "dependencies": {
  1143 + "@babel/runtime": "^7.18.0",
  1144 + "@rc-component/portal": "^1.0.0-9",
  1145 + "@rc-component/trigger": "^2.0.0",
  1146 + "classnames": "^2.3.2",
  1147 + "rc-util": "^5.24.4"
  1148 + },
  1149 + "engines": {
  1150 + "node": ">=8.x"
  1151 + },
  1152 + "peerDependencies": {
  1153 + "react": ">=16.9.0",
  1154 + "react-dom": ">=16.9.0"
  1155 + }
  1156 + },
  1157 + "node_modules/@rc-component/trigger": {
  1158 + "version": "2.3.1",
  1159 + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.1.tgz",
  1160 + "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==",
  1161 + "license": "MIT",
  1162 + "dependencies": {
  1163 + "@babel/runtime": "^7.23.2",
  1164 + "@rc-component/portal": "^1.1.0",
  1165 + "classnames": "^2.3.2",
  1166 + "rc-motion": "^2.0.0",
  1167 + "rc-resize-observer": "^1.3.1",
  1168 + "rc-util": "^5.44.0"
  1169 + },
  1170 + "engines": {
  1171 + "node": ">=8.x"
  1172 + },
  1173 + "peerDependencies": {
  1174 + "react": ">=16.9.0",
  1175 + "react-dom": ">=16.9.0"
  1176 + }
  1177 + },
  1178 + "node_modules/@reduxjs/toolkit": {
  1179 + "version": "2.11.2",
  1180 + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
  1181 + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
  1182 + "license": "MIT",
  1183 + "dependencies": {
  1184 + "@standard-schema/spec": "^1.0.0",
  1185 + "@standard-schema/utils": "^0.3.0",
  1186 + "immer": "^11.0.0",
  1187 + "redux": "^5.0.1",
  1188 + "redux-thunk": "^3.1.0",
  1189 + "reselect": "^5.1.0"
  1190 + },
  1191 + "peerDependencies": {
  1192 + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
  1193 + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
  1194 + },
  1195 + "peerDependenciesMeta": {
  1196 + "react": {
  1197 + "optional": true
  1198 + },
  1199 + "react-redux": {
  1200 + "optional": true
  1201 + }
  1202 + }
  1203 + },
  1204 + "node_modules/@remix-run/router": {
  1205 + "version": "1.23.2",
  1206 + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
  1207 + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
  1208 + "license": "MIT",
  1209 + "engines": {
  1210 + "node": ">=14.0.0"
  1211 + }
  1212 + },
  1213 + "node_modules/@rolldown/pluginutils": {
  1214 + "version": "1.0.0-beta.27",
  1215 + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
  1216 + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
  1217 + "dev": true,
  1218 + "license": "MIT"
  1219 + },
  1220 + "node_modules/@rollup/rollup-android-arm-eabi": {
  1221 + "version": "4.60.3",
  1222 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
  1223 + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==",
  1224 + "cpu": [
  1225 + "arm"
  1226 + ],
  1227 + "dev": true,
  1228 + "license": "MIT",
  1229 + "optional": true,
  1230 + "os": [
  1231 + "android"
  1232 + ]
  1233 + },
  1234 + "node_modules/@rollup/rollup-android-arm64": {
  1235 + "version": "4.60.3",
  1236 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz",
  1237 + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==",
  1238 + "cpu": [
  1239 + "arm64"
  1240 + ],
  1241 + "dev": true,
  1242 + "license": "MIT",
  1243 + "optional": true,
  1244 + "os": [
  1245 + "android"
  1246 + ]
  1247 + },
  1248 + "node_modules/@rollup/rollup-darwin-arm64": {
  1249 + "version": "4.60.3",
  1250 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz",
  1251 + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==",
  1252 + "cpu": [
  1253 + "arm64"
  1254 + ],
  1255 + "dev": true,
  1256 + "license": "MIT",
  1257 + "optional": true,
  1258 + "os": [
  1259 + "darwin"
  1260 + ]
  1261 + },
  1262 + "node_modules/@rollup/rollup-darwin-x64": {
  1263 + "version": "4.60.3",
  1264 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz",
  1265 + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==",
  1266 + "cpu": [
  1267 + "x64"
  1268 + ],
  1269 + "dev": true,
  1270 + "license": "MIT",
  1271 + "optional": true,
  1272 + "os": [
  1273 + "darwin"
  1274 + ]
  1275 + },
  1276 + "node_modules/@rollup/rollup-freebsd-arm64": {
  1277 + "version": "4.60.3",
  1278 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz",
  1279 + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==",
  1280 + "cpu": [
  1281 + "arm64"
  1282 + ],
  1283 + "dev": true,
  1284 + "license": "MIT",
  1285 + "optional": true,
  1286 + "os": [
  1287 + "freebsd"
  1288 + ]
  1289 + },
  1290 + "node_modules/@rollup/rollup-freebsd-x64": {
  1291 + "version": "4.60.3",
  1292 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz",
  1293 + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==",
  1294 + "cpu": [
  1295 + "x64"
  1296 + ],
  1297 + "dev": true,
  1298 + "license": "MIT",
  1299 + "optional": true,
  1300 + "os": [
  1301 + "freebsd"
  1302 + ]
  1303 + },
  1304 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
  1305 + "version": "4.60.3",
  1306 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz",
  1307 + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==",
  1308 + "cpu": [
  1309 + "arm"
  1310 + ],
  1311 + "dev": true,
  1312 + "libc": [
  1313 + "glibc"
  1314 + ],
  1315 + "license": "MIT",
  1316 + "optional": true,
  1317 + "os": [
  1318 + "linux"
  1319 + ]
  1320 + },
  1321 + "node_modules/@rollup/rollup-linux-arm-musleabihf": {
  1322 + "version": "4.60.3",
  1323 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz",
  1324 + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==",
  1325 + "cpu": [
  1326 + "arm"
  1327 + ],
  1328 + "dev": true,
  1329 + "libc": [
  1330 + "musl"
  1331 + ],
  1332 + "license": "MIT",
  1333 + "optional": true,
  1334 + "os": [
  1335 + "linux"
  1336 + ]
  1337 + },
  1338 + "node_modules/@rollup/rollup-linux-arm64-gnu": {
  1339 + "version": "4.60.3",
  1340 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz",
  1341 + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==",
  1342 + "cpu": [
  1343 + "arm64"
  1344 + ],
  1345 + "dev": true,
  1346 + "libc": [
  1347 + "glibc"
  1348 + ],
  1349 + "license": "MIT",
  1350 + "optional": true,
  1351 + "os": [
  1352 + "linux"
  1353 + ]
  1354 + },
  1355 + "node_modules/@rollup/rollup-linux-arm64-musl": {
  1356 + "version": "4.60.3",
  1357 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz",
  1358 + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==",
  1359 + "cpu": [
  1360 + "arm64"
  1361 + ],
  1362 + "dev": true,
  1363 + "libc": [
  1364 + "musl"
  1365 + ],
  1366 + "license": "MIT",
  1367 + "optional": true,
  1368 + "os": [
  1369 + "linux"
  1370 + ]
  1371 + },
  1372 + "node_modules/@rollup/rollup-linux-loong64-gnu": {
  1373 + "version": "4.60.3",
  1374 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz",
  1375 + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==",
  1376 + "cpu": [
  1377 + "loong64"
  1378 + ],
  1379 + "dev": true,
  1380 + "libc": [
  1381 + "glibc"
  1382 + ],
  1383 + "license": "MIT",
  1384 + "optional": true,
  1385 + "os": [
  1386 + "linux"
  1387 + ]
  1388 + },
  1389 + "node_modules/@rollup/rollup-linux-loong64-musl": {
  1390 + "version": "4.60.3",
  1391 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz",
  1392 + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==",
  1393 + "cpu": [
  1394 + "loong64"
  1395 + ],
  1396 + "dev": true,
  1397 + "libc": [
  1398 + "musl"
  1399 + ],
  1400 + "license": "MIT",
  1401 + "optional": true,
  1402 + "os": [
  1403 + "linux"
  1404 + ]
  1405 + },
  1406 + "node_modules/@rollup/rollup-linux-ppc64-gnu": {
  1407 + "version": "4.60.3",
  1408 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz",
  1409 + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==",
  1410 + "cpu": [
  1411 + "ppc64"
  1412 + ],
  1413 + "dev": true,
  1414 + "libc": [
  1415 + "glibc"
  1416 + ],
  1417 + "license": "MIT",
  1418 + "optional": true,
  1419 + "os": [
  1420 + "linux"
  1421 + ]
  1422 + },
  1423 + "node_modules/@rollup/rollup-linux-ppc64-musl": {
  1424 + "version": "4.60.3",
  1425 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz",
  1426 + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==",
  1427 + "cpu": [
  1428 + "ppc64"
  1429 + ],
  1430 + "dev": true,
  1431 + "libc": [
  1432 + "musl"
  1433 + ],
  1434 + "license": "MIT",
  1435 + "optional": true,
  1436 + "os": [
  1437 + "linux"
  1438 + ]
  1439 + },
  1440 + "node_modules/@rollup/rollup-linux-riscv64-gnu": {
  1441 + "version": "4.60.3",
  1442 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz",
  1443 + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==",
  1444 + "cpu": [
  1445 + "riscv64"
  1446 + ],
  1447 + "dev": true,
  1448 + "libc": [
  1449 + "glibc"
  1450 + ],
  1451 + "license": "MIT",
  1452 + "optional": true,
  1453 + "os": [
  1454 + "linux"
  1455 + ]
  1456 + },
  1457 + "node_modules/@rollup/rollup-linux-riscv64-musl": {
  1458 + "version": "4.60.3",
  1459 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz",
  1460 + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==",
  1461 + "cpu": [
  1462 + "riscv64"
  1463 + ],
  1464 + "dev": true,
  1465 + "libc": [
  1466 + "musl"
  1467 + ],
  1468 + "license": "MIT",
  1469 + "optional": true,
  1470 + "os": [
  1471 + "linux"
  1472 + ]
  1473 + },
  1474 + "node_modules/@rollup/rollup-linux-s390x-gnu": {
  1475 + "version": "4.60.3",
  1476 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz",
  1477 + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==",
  1478 + "cpu": [
  1479 + "s390x"
  1480 + ],
  1481 + "dev": true,
  1482 + "libc": [
  1483 + "glibc"
  1484 + ],
  1485 + "license": "MIT",
  1486 + "optional": true,
  1487 + "os": [
  1488 + "linux"
  1489 + ]
  1490 + },
  1491 + "node_modules/@rollup/rollup-linux-x64-gnu": {
  1492 + "version": "4.60.3",
  1493 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz",
  1494 + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==",
  1495 + "cpu": [
  1496 + "x64"
  1497 + ],
  1498 + "dev": true,
  1499 + "libc": [
  1500 + "glibc"
  1501 + ],
  1502 + "license": "MIT",
  1503 + "optional": true,
  1504 + "os": [
  1505 + "linux"
  1506 + ]
  1507 + },
  1508 + "node_modules/@rollup/rollup-linux-x64-musl": {
  1509 + "version": "4.60.3",
  1510 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz",
  1511 + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==",
  1512 + "cpu": [
  1513 + "x64"
  1514 + ],
  1515 + "dev": true,
  1516 + "libc": [
  1517 + "musl"
  1518 + ],
  1519 + "license": "MIT",
  1520 + "optional": true,
  1521 + "os": [
  1522 + "linux"
  1523 + ]
  1524 + },
  1525 + "node_modules/@rollup/rollup-openbsd-x64": {
  1526 + "version": "4.60.3",
  1527 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz",
  1528 + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==",
  1529 + "cpu": [
  1530 + "x64"
  1531 + ],
  1532 + "dev": true,
  1533 + "license": "MIT",
  1534 + "optional": true,
  1535 + "os": [
  1536 + "openbsd"
  1537 + ]
  1538 + },
  1539 + "node_modules/@rollup/rollup-openharmony-arm64": {
  1540 + "version": "4.60.3",
  1541 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz",
  1542 + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==",
  1543 + "cpu": [
  1544 + "arm64"
  1545 + ],
  1546 + "dev": true,
  1547 + "license": "MIT",
  1548 + "optional": true,
  1549 + "os": [
  1550 + "openharmony"
  1551 + ]
  1552 + },
  1553 + "node_modules/@rollup/rollup-win32-arm64-msvc": {
  1554 + "version": "4.60.3",
  1555 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz",
  1556 + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==",
  1557 + "cpu": [
  1558 + "arm64"
  1559 + ],
  1560 + "dev": true,
  1561 + "license": "MIT",
  1562 + "optional": true,
  1563 + "os": [
  1564 + "win32"
  1565 + ]
  1566 + },
  1567 + "node_modules/@rollup/rollup-win32-ia32-msvc": {
  1568 + "version": "4.60.3",
  1569 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz",
  1570 + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==",
  1571 + "cpu": [
  1572 + "ia32"
  1573 + ],
  1574 + "dev": true,
  1575 + "license": "MIT",
  1576 + "optional": true,
  1577 + "os": [
  1578 + "win32"
  1579 + ]
  1580 + },
  1581 + "node_modules/@rollup/rollup-win32-x64-gnu": {
  1582 + "version": "4.60.3",
  1583 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz",
  1584 + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==",
  1585 + "cpu": [
  1586 + "x64"
  1587 + ],
  1588 + "dev": true,
  1589 + "license": "MIT",
  1590 + "optional": true,
  1591 + "os": [
  1592 + "win32"
  1593 + ]
  1594 + },
  1595 + "node_modules/@rollup/rollup-win32-x64-msvc": {
  1596 + "version": "4.60.3",
  1597 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz",
  1598 + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==",
  1599 + "cpu": [
  1600 + "x64"
  1601 + ],
  1602 + "dev": true,
  1603 + "license": "MIT",
  1604 + "optional": true,
  1605 + "os": [
  1606 + "win32"
  1607 + ]
  1608 + },
  1609 + "node_modules/@sinclair/typebox": {
  1610 + "version": "0.27.10",
  1611 + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz",
  1612 + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==",
  1613 + "dev": true,
  1614 + "license": "MIT"
  1615 + },
  1616 + "node_modules/@standard-schema/spec": {
  1617 + "version": "1.1.0",
  1618 + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
  1619 + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
  1620 + "license": "MIT"
  1621 + },
  1622 + "node_modules/@standard-schema/utils": {
  1623 + "version": "0.3.0",
  1624 + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
  1625 + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
  1626 + "license": "MIT"
  1627 + },
  1628 + "node_modules/@testing-library/dom": {
  1629 + "version": "10.4.1",
  1630 + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
  1631 + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
  1632 + "dev": true,
  1633 + "license": "MIT",
  1634 + "peer": true,
  1635 + "dependencies": {
  1636 + "@babel/code-frame": "^7.10.4",
  1637 + "@babel/runtime": "^7.12.5",
  1638 + "@types/aria-query": "^5.0.1",
  1639 + "aria-query": "5.3.0",
  1640 + "dom-accessibility-api": "^0.5.9",
  1641 + "lz-string": "^1.5.0",
  1642 + "picocolors": "1.1.1",
  1643 + "pretty-format": "^27.0.2"
  1644 + },
  1645 + "engines": {
  1646 + "node": ">=18"
  1647 + }
  1648 + },
  1649 + "node_modules/@testing-library/jest-dom": {
  1650 + "version": "6.9.1",
  1651 + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
  1652 + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
  1653 + "dev": true,
  1654 + "license": "MIT",
  1655 + "dependencies": {
  1656 + "@adobe/css-tools": "^4.4.0",
  1657 + "aria-query": "^5.0.0",
  1658 + "css.escape": "^1.5.1",
  1659 + "dom-accessibility-api": "^0.6.3",
  1660 + "picocolors": "^1.1.1",
  1661 + "redent": "^3.0.0"
  1662 + },
  1663 + "engines": {
  1664 + "node": ">=14",
  1665 + "npm": ">=6",
  1666 + "yarn": ">=1"
  1667 + }
  1668 + },
  1669 + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
  1670 + "version": "0.6.3",
  1671 + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
  1672 + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
  1673 + "dev": true,
  1674 + "license": "MIT"
  1675 + },
  1676 + "node_modules/@testing-library/react": {
  1677 + "version": "16.3.2",
  1678 + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
  1679 + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
  1680 + "dev": true,
  1681 + "license": "MIT",
  1682 + "dependencies": {
  1683 + "@babel/runtime": "^7.12.5"
  1684 + },
  1685 + "engines": {
  1686 + "node": ">=18"
  1687 + },
  1688 + "peerDependencies": {
  1689 + "@testing-library/dom": "^10.0.0",
  1690 + "@types/react": "^18.0.0 || ^19.0.0",
  1691 + "@types/react-dom": "^18.0.0 || ^19.0.0",
  1692 + "react": "^18.0.0 || ^19.0.0",
  1693 + "react-dom": "^18.0.0 || ^19.0.0"
  1694 + },
  1695 + "peerDependenciesMeta": {
  1696 + "@types/react": {
  1697 + "optional": true
  1698 + },
  1699 + "@types/react-dom": {
  1700 + "optional": true
  1701 + }
  1702 + }
  1703 + },
  1704 + "node_modules/@testing-library/user-event": {
  1705 + "version": "14.6.1",
  1706 + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
  1707 + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
  1708 + "dev": true,
  1709 + "license": "MIT",
  1710 + "engines": {
  1711 + "node": ">=12",
  1712 + "npm": ">=6"
  1713 + },
  1714 + "peerDependencies": {
  1715 + "@testing-library/dom": ">=7.21.4"
  1716 + }
  1717 + },
  1718 + "node_modules/@types/aria-query": {
  1719 + "version": "5.0.4",
  1720 + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
  1721 + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
  1722 + "dev": true,
  1723 + "license": "MIT",
  1724 + "peer": true
  1725 + },
  1726 + "node_modules/@types/babel__core": {
  1727 + "version": "7.20.5",
  1728 + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
  1729 + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
  1730 + "dev": true,
  1731 + "license": "MIT",
  1732 + "dependencies": {
  1733 + "@babel/parser": "^7.20.7",
  1734 + "@babel/types": "^7.20.7",
  1735 + "@types/babel__generator": "*",
  1736 + "@types/babel__template": "*",
  1737 + "@types/babel__traverse": "*"
  1738 + }
  1739 + },
  1740 + "node_modules/@types/babel__generator": {
  1741 + "version": "7.27.0",
  1742 + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
  1743 + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
  1744 + "dev": true,
  1745 + "license": "MIT",
  1746 + "dependencies": {
  1747 + "@babel/types": "^7.0.0"
  1748 + }
  1749 + },
  1750 + "node_modules/@types/babel__template": {
  1751 + "version": "7.4.4",
  1752 + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
  1753 + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
  1754 + "dev": true,
  1755 + "license": "MIT",
  1756 + "dependencies": {
  1757 + "@babel/parser": "^7.1.0",
  1758 + "@babel/types": "^7.0.0"
  1759 + }
  1760 + },
  1761 + "node_modules/@types/babel__traverse": {
  1762 + "version": "7.28.0",
  1763 + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
  1764 + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
  1765 + "dev": true,
  1766 + "license": "MIT",
  1767 + "dependencies": {
  1768 + "@babel/types": "^7.28.2"
  1769 + }
  1770 + },
  1771 + "node_modules/@types/estree": {
  1772 + "version": "1.0.8",
  1773 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
  1774 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
  1775 + "dev": true,
  1776 + "license": "MIT"
  1777 + },
  1778 + "node_modules/@types/prop-types": {
  1779 + "version": "15.7.15",
  1780 + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
  1781 + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
  1782 + "devOptional": true,
  1783 + "license": "MIT"
  1784 + },
  1785 + "node_modules/@types/react": {
  1786 + "version": "18.3.28",
  1787 + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
  1788 + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
  1789 + "devOptional": true,
  1790 + "license": "MIT",
  1791 + "dependencies": {
  1792 + "@types/prop-types": "*",
  1793 + "csstype": "^3.2.2"
  1794 + }
  1795 + },
  1796 + "node_modules/@types/react-dom": {
  1797 + "version": "18.3.7",
  1798 + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
  1799 + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
  1800 + "dev": true,
  1801 + "license": "MIT",
  1802 + "peerDependencies": {
  1803 + "@types/react": "^18.0.0"
  1804 + }
  1805 + },
  1806 + "node_modules/@types/use-sync-external-store": {
  1807 + "version": "0.0.6",
  1808 + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
  1809 + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
  1810 + "license": "MIT"
  1811 + },
  1812 + "node_modules/@vitejs/plugin-react": {
  1813 + "version": "4.7.0",
  1814 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
  1815 + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
  1816 + "dev": true,
  1817 + "license": "MIT",
  1818 + "dependencies": {
  1819 + "@babel/core": "^7.28.0",
  1820 + "@babel/plugin-transform-react-jsx-self": "^7.27.1",
  1821 + "@babel/plugin-transform-react-jsx-source": "^7.27.1",
  1822 + "@rolldown/pluginutils": "1.0.0-beta.27",
  1823 + "@types/babel__core": "^7.20.5",
  1824 + "react-refresh": "^0.17.0"
  1825 + },
  1826 + "engines": {
  1827 + "node": "^14.18.0 || >=16.0.0"
  1828 + },
  1829 + "peerDependencies": {
  1830 + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
  1831 + }
  1832 + },
  1833 + "node_modules/@vitest/expect": {
  1834 + "version": "1.6.1",
  1835 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
  1836 + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==",
  1837 + "dev": true,
  1838 + "license": "MIT",
  1839 + "dependencies": {
  1840 + "@vitest/spy": "1.6.1",
  1841 + "@vitest/utils": "1.6.1",
  1842 + "chai": "^4.3.10"
  1843 + },
  1844 + "funding": {
  1845 + "url": "https://opencollective.com/vitest"
  1846 + }
  1847 + },
  1848 + "node_modules/@vitest/runner": {
  1849 + "version": "1.6.1",
  1850 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz",
  1851 + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==",
  1852 + "dev": true,
  1853 + "license": "MIT",
  1854 + "dependencies": {
  1855 + "@vitest/utils": "1.6.1",
  1856 + "p-limit": "^5.0.0",
  1857 + "pathe": "^1.1.1"
  1858 + },
  1859 + "funding": {
  1860 + "url": "https://opencollective.com/vitest"
  1861 + }
  1862 + },
  1863 + "node_modules/@vitest/snapshot": {
  1864 + "version": "1.6.1",
  1865 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz",
  1866 + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==",
  1867 + "dev": true,
  1868 + "license": "MIT",
  1869 + "dependencies": {
  1870 + "magic-string": "^0.30.5",
  1871 + "pathe": "^1.1.1",
  1872 + "pretty-format": "^29.7.0"
  1873 + },
  1874 + "funding": {
  1875 + "url": "https://opencollective.com/vitest"
  1876 + }
  1877 + },
  1878 + "node_modules/@vitest/snapshot/node_modules/pretty-format": {
  1879 + "version": "29.7.0",
  1880 + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
  1881 + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
  1882 + "dev": true,
  1883 + "license": "MIT",
  1884 + "dependencies": {
  1885 + "@jest/schemas": "^29.6.3",
  1886 + "ansi-styles": "^5.0.0",
  1887 + "react-is": "^18.0.0"
  1888 + },
  1889 + "engines": {
  1890 + "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
  1891 + }
  1892 + },
  1893 + "node_modules/@vitest/snapshot/node_modules/react-is": {
  1894 + "version": "18.3.1",
  1895 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
  1896 + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
  1897 + "dev": true,
  1898 + "license": "MIT"
  1899 + },
  1900 + "node_modules/@vitest/spy": {
  1901 + "version": "1.6.1",
  1902 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz",
  1903 + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==",
  1904 + "dev": true,
  1905 + "license": "MIT",
  1906 + "dependencies": {
  1907 + "tinyspy": "^2.2.0"
  1908 + },
  1909 + "funding": {
  1910 + "url": "https://opencollective.com/vitest"
  1911 + }
  1912 + },
  1913 + "node_modules/@vitest/utils": {
  1914 + "version": "1.6.1",
  1915 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz",
  1916 + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==",
  1917 + "dev": true,
  1918 + "license": "MIT",
  1919 + "dependencies": {
  1920 + "diff-sequences": "^29.6.3",
  1921 + "estree-walker": "^3.0.3",
  1922 + "loupe": "^2.3.7",
  1923 + "pretty-format": "^29.7.0"
  1924 + },
  1925 + "funding": {
  1926 + "url": "https://opencollective.com/vitest"
  1927 + }
  1928 + },
  1929 + "node_modules/@vitest/utils/node_modules/pretty-format": {
  1930 + "version": "29.7.0",
  1931 + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
  1932 + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
  1933 + "dev": true,
  1934 + "license": "MIT",
  1935 + "dependencies": {
  1936 + "@jest/schemas": "^29.6.3",
  1937 + "ansi-styles": "^5.0.0",
  1938 + "react-is": "^18.0.0"
  1939 + },
  1940 + "engines": {
  1941 + "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
  1942 + }
  1943 + },
  1944 + "node_modules/@vitest/utils/node_modules/react-is": {
  1945 + "version": "18.3.1",
  1946 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
  1947 + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
  1948 + "dev": true,
  1949 + "license": "MIT"
  1950 + },
  1951 + "node_modules/acorn": {
  1952 + "version": "8.16.0",
  1953 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
  1954 + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
  1955 + "dev": true,
  1956 + "license": "MIT",
  1957 + "bin": {
  1958 + "acorn": "bin/acorn"
  1959 + },
  1960 + "engines": {
  1961 + "node": ">=0.4.0"
  1962 + }
  1963 + },
  1964 + "node_modules/acorn-walk": {
  1965 + "version": "8.3.5",
  1966 + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
  1967 + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
  1968 + "dev": true,
  1969 + "license": "MIT",
  1970 + "dependencies": {
  1971 + "acorn": "^8.11.0"
  1972 + },
  1973 + "engines": {
  1974 + "node": ">=0.4.0"
  1975 + }
  1976 + },
  1977 + "node_modules/agent-base": {
  1978 + "version": "7.1.4",
  1979 + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
  1980 + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
  1981 + "dev": true,
  1982 + "license": "MIT",
  1983 + "engines": {
  1984 + "node": ">= 14"
  1985 + }
  1986 + },
  1987 + "node_modules/ansi-regex": {
  1988 + "version": "5.0.1",
  1989 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
  1990 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
  1991 + "dev": true,
  1992 + "license": "MIT",
  1993 + "peer": true,
  1994 + "engines": {
  1995 + "node": ">=8"
  1996 + }
  1997 + },
  1998 + "node_modules/ansi-styles": {
  1999 + "version": "5.2.0",
  2000 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
  2001 + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
  2002 + "dev": true,
  2003 + "license": "MIT",
  2004 + "engines": {
  2005 + "node": ">=10"
  2006 + },
  2007 + "funding": {
  2008 + "url": "https://github.com/chalk/ansi-styles?sponsor=1"
  2009 + }
  2010 + },
  2011 + "node_modules/antd": {
  2012 + "version": "5.29.3",
  2013 + "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz",
  2014 + "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==",
  2015 + "license": "MIT",
  2016 + "dependencies": {
  2017 + "@ant-design/colors": "^7.2.1",
  2018 + "@ant-design/cssinjs": "^1.23.0",
  2019 + "@ant-design/cssinjs-utils": "^1.1.3",
  2020 + "@ant-design/fast-color": "^2.0.6",
  2021 + "@ant-design/icons": "^5.6.1",
  2022 + "@ant-design/react-slick": "~1.1.2",
  2023 + "@babel/runtime": "^7.26.0",
  2024 + "@rc-component/color-picker": "~2.0.1",
  2025 + "@rc-component/mutate-observer": "^1.1.0",
  2026 + "@rc-component/qrcode": "~1.1.0",
  2027 + "@rc-component/tour": "~1.15.1",
  2028 + "@rc-component/trigger": "^2.3.0",
  2029 + "classnames": "^2.5.1",
  2030 + "copy-to-clipboard": "^3.3.3",
  2031 + "dayjs": "^1.11.11",
  2032 + "rc-cascader": "~3.34.0",
  2033 + "rc-checkbox": "~3.5.0",
  2034 + "rc-collapse": "~3.9.0",
  2035 + "rc-dialog": "~9.6.0",
  2036 + "rc-drawer": "~7.3.0",
  2037 + "rc-dropdown": "~4.2.1",
  2038 + "rc-field-form": "~2.7.1",
  2039 + "rc-image": "~7.12.0",
  2040 + "rc-input": "~1.8.0",
  2041 + "rc-input-number": "~9.5.0",
  2042 + "rc-mentions": "~2.20.0",
  2043 + "rc-menu": "~9.16.1",
  2044 + "rc-motion": "^2.9.5",
  2045 + "rc-notification": "~5.6.4",
  2046 + "rc-pagination": "~5.1.0",
  2047 + "rc-picker": "~4.11.3",
  2048 + "rc-progress": "~4.0.0",
  2049 + "rc-rate": "~2.13.1",
  2050 + "rc-resize-observer": "^1.4.3",
  2051 + "rc-segmented": "~2.7.0",
  2052 + "rc-select": "~14.16.8",
  2053 + "rc-slider": "~11.1.9",
  2054 + "rc-steps": "~6.0.1",
  2055 + "rc-switch": "~4.1.0",
  2056 + "rc-table": "~7.54.0",
  2057 + "rc-tabs": "~15.7.0",
  2058 + "rc-textarea": "~1.10.2",
  2059 + "rc-tooltip": "~6.4.0",
  2060 + "rc-tree": "~5.13.1",
  2061 + "rc-tree-select": "~5.27.0",
  2062 + "rc-upload": "~4.11.0",
  2063 + "rc-util": "^5.44.4",
  2064 + "scroll-into-view-if-needed": "^3.1.0",
  2065 + "throttle-debounce": "^5.0.2"
  2066 + },
  2067 + "funding": {
  2068 + "type": "opencollective",
  2069 + "url": "https://opencollective.com/ant-design"
  2070 + },
  2071 + "peerDependencies": {
  2072 + "react": ">=16.9.0",
  2073 + "react-dom": ">=16.9.0"
  2074 + }
  2075 + },
  2076 + "node_modules/aria-query": {
  2077 + "version": "5.3.0",
  2078 + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
  2079 + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
  2080 + "dev": true,
  2081 + "license": "Apache-2.0",
  2082 + "dependencies": {
  2083 + "dequal": "^2.0.3"
  2084 + }
  2085 + },
  2086 + "node_modules/assertion-error": {
  2087 + "version": "1.1.0",
  2088 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
  2089 + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
  2090 + "dev": true,
  2091 + "license": "MIT",
  2092 + "engines": {
  2093 + "node": "*"
  2094 + }
  2095 + },
  2096 + "node_modules/asynckit": {
  2097 + "version": "0.4.0",
  2098 + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
  2099 + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
  2100 + "license": "MIT"
  2101 + },
  2102 + "node_modules/axios": {
  2103 + "version": "1.16.0",
  2104 + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz",
  2105 + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==",
  2106 + "license": "MIT",
  2107 + "dependencies": {
  2108 + "follow-redirects": "^1.16.0",
  2109 + "form-data": "^4.0.5",
  2110 + "proxy-from-env": "^2.1.0"
  2111 + }
  2112 + },
  2113 + "node_modules/baseline-browser-mapping": {
  2114 + "version": "2.10.27",
  2115 + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz",
  2116 + "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==",
  2117 + "dev": true,
  2118 + "license": "Apache-2.0",
  2119 + "bin": {
  2120 + "baseline-browser-mapping": "dist/cli.cjs"
  2121 + },
  2122 + "engines": {
  2123 + "node": ">=6.0.0"
  2124 + }
  2125 + },
  2126 + "node_modules/browserslist": {
  2127 + "version": "4.28.2",
  2128 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
  2129 + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
  2130 + "dev": true,
  2131 + "funding": [
  2132 + {
  2133 + "type": "opencollective",
  2134 + "url": "https://opencollective.com/browserslist"
  2135 + },
  2136 + {
  2137 + "type": "tidelift",
  2138 + "url": "https://tidelift.com/funding/github/npm/browserslist"
  2139 + },
  2140 + {
  2141 + "type": "github",
  2142 + "url": "https://github.com/sponsors/ai"
  2143 + }
  2144 + ],
  2145 + "license": "MIT",
  2146 + "dependencies": {
  2147 + "baseline-browser-mapping": "^2.10.12",
  2148 + "caniuse-lite": "^1.0.30001782",
  2149 + "electron-to-chromium": "^1.5.328",
  2150 + "node-releases": "^2.0.36",
  2151 + "update-browserslist-db": "^1.2.3"
  2152 + },
  2153 + "bin": {
  2154 + "browserslist": "cli.js"
  2155 + },
  2156 + "engines": {
  2157 + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
  2158 + }
  2159 + },
  2160 + "node_modules/cac": {
  2161 + "version": "6.7.14",
  2162 + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
  2163 + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
  2164 + "dev": true,
  2165 + "license": "MIT",
  2166 + "engines": {
  2167 + "node": ">=8"
  2168 + }
  2169 + },
  2170 + "node_modules/call-bind-apply-helpers": {
  2171 + "version": "1.0.2",
  2172 + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
  2173 + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
  2174 + "license": "MIT",
  2175 + "dependencies": {
  2176 + "es-errors": "^1.3.0",
  2177 + "function-bind": "^1.1.2"
  2178 + },
  2179 + "engines": {
  2180 + "node": ">= 0.4"
  2181 + }
  2182 + },
  2183 + "node_modules/caniuse-lite": {
  2184 + "version": "1.0.30001792",
  2185 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz",
  2186 + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==",
  2187 + "dev": true,
  2188 + "funding": [
  2189 + {
  2190 + "type": "opencollective",
  2191 + "url": "https://opencollective.com/browserslist"
  2192 + },
  2193 + {
  2194 + "type": "tidelift",
  2195 + "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
  2196 + },
  2197 + {
  2198 + "type": "github",
  2199 + "url": "https://github.com/sponsors/ai"
  2200 + }
  2201 + ],
  2202 + "license": "CC-BY-4.0"
  2203 + },
  2204 + "node_modules/chai": {
  2205 + "version": "4.5.0",
  2206 + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
  2207 + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==",
  2208 + "dev": true,
  2209 + "license": "MIT",
  2210 + "dependencies": {
  2211 + "assertion-error": "^1.1.0",
  2212 + "check-error": "^1.0.3",
  2213 + "deep-eql": "^4.1.3",
  2214 + "get-func-name": "^2.0.2",
  2215 + "loupe": "^2.3.6",
  2216 + "pathval": "^1.1.1",
  2217 + "type-detect": "^4.1.0"
  2218 + },
  2219 + "engines": {
  2220 + "node": ">=4"
  2221 + }
  2222 + },
  2223 + "node_modules/check-error": {
  2224 + "version": "1.0.3",
  2225 + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
  2226 + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
  2227 + "dev": true,
  2228 + "license": "MIT",
  2229 + "dependencies": {
  2230 + "get-func-name": "^2.0.2"
  2231 + },
  2232 + "engines": {
  2233 + "node": "*"
  2234 + }
  2235 + },
  2236 + "node_modules/classnames": {
  2237 + "version": "2.5.1",
  2238 + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
  2239 + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
  2240 + "license": "MIT"
  2241 + },
  2242 + "node_modules/combined-stream": {
  2243 + "version": "1.0.8",
  2244 + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
  2245 + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
  2246 + "license": "MIT",
  2247 + "dependencies": {
  2248 + "delayed-stream": "~1.0.0"
  2249 + },
  2250 + "engines": {
  2251 + "node": ">= 0.8"
  2252 + }
  2253 + },
  2254 + "node_modules/compute-scroll-into-view": {
  2255 + "version": "3.1.1",
  2256 + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
  2257 + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
  2258 + "license": "MIT"
  2259 + },
  2260 + "node_modules/confbox": {
  2261 + "version": "0.1.8",
  2262 + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
  2263 + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
  2264 + "dev": true,
  2265 + "license": "MIT"
  2266 + },
  2267 + "node_modules/convert-source-map": {
  2268 + "version": "2.0.0",
  2269 + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
  2270 + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
  2271 + "dev": true,
  2272 + "license": "MIT"
  2273 + },
  2274 + "node_modules/copy-to-clipboard": {
  2275 + "version": "3.3.3",
  2276 + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
  2277 + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
  2278 + "license": "MIT",
  2279 + "dependencies": {
  2280 + "toggle-selection": "^1.0.6"
  2281 + }
  2282 + },
  2283 + "node_modules/cross-spawn": {
  2284 + "version": "7.0.6",
  2285 + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
  2286 + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
  2287 + "dev": true,
  2288 + "license": "MIT",
  2289 + "dependencies": {
  2290 + "path-key": "^3.1.0",
  2291 + "shebang-command": "^2.0.0",
  2292 + "which": "^2.0.1"
  2293 + },
  2294 + "engines": {
  2295 + "node": ">= 8"
  2296 + }
  2297 + },
  2298 + "node_modules/css.escape": {
  2299 + "version": "1.5.1",
  2300 + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
  2301 + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
  2302 + "dev": true,
  2303 + "license": "MIT"
  2304 + },
  2305 + "node_modules/cssstyle": {
  2306 + "version": "4.6.0",
  2307 + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
  2308 + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
  2309 + "dev": true,
  2310 + "license": "MIT",
  2311 + "dependencies": {
  2312 + "@asamuzakjp/css-color": "^3.2.0",
  2313 + "rrweb-cssom": "^0.8.0"
  2314 + },
  2315 + "engines": {
  2316 + "node": ">=18"
  2317 + }
  2318 + },
  2319 + "node_modules/cssstyle/node_modules/rrweb-cssom": {
  2320 + "version": "0.8.0",
  2321 + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
  2322 + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
  2323 + "dev": true,
  2324 + "license": "MIT"
  2325 + },
  2326 + "node_modules/csstype": {
  2327 + "version": "3.2.3",
  2328 + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
  2329 + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
  2330 + "license": "MIT"
  2331 + },
  2332 + "node_modules/data-urls": {
  2333 + "version": "5.0.0",
  2334 + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
  2335 + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
  2336 + "dev": true,
  2337 + "license": "MIT",
  2338 + "dependencies": {
  2339 + "whatwg-mimetype": "^4.0.0",
  2340 + "whatwg-url": "^14.0.0"
  2341 + },
  2342 + "engines": {
  2343 + "node": ">=18"
  2344 + }
  2345 + },
  2346 + "node_modules/dayjs": {
  2347 + "version": "1.11.20",
  2348 + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
  2349 + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
  2350 + "license": "MIT"
  2351 + },
  2352 + "node_modules/debug": {
  2353 + "version": "4.4.3",
  2354 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
  2355 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
  2356 + "dev": true,
  2357 + "license": "MIT",
  2358 + "dependencies": {
  2359 + "ms": "^2.1.3"
  2360 + },
  2361 + "engines": {
  2362 + "node": ">=6.0"
  2363 + },
  2364 + "peerDependenciesMeta": {
  2365 + "supports-color": {
  2366 + "optional": true
  2367 + }
  2368 + }
  2369 + },
  2370 + "node_modules/decimal.js": {
  2371 + "version": "10.6.0",
  2372 + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
  2373 + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
  2374 + "dev": true,
  2375 + "license": "MIT"
  2376 + },
  2377 + "node_modules/deep-eql": {
  2378 + "version": "4.1.4",
  2379 + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz",
  2380 + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==",
  2381 + "dev": true,
  2382 + "license": "MIT",
  2383 + "dependencies": {
  2384 + "type-detect": "^4.0.0"
  2385 + },
  2386 + "engines": {
  2387 + "node": ">=6"
  2388 + }
  2389 + },
  2390 + "node_modules/delayed-stream": {
  2391 + "version": "1.0.0",
  2392 + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
  2393 + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
  2394 + "license": "MIT",
  2395 + "engines": {
  2396 + "node": ">=0.4.0"
  2397 + }
  2398 + },
  2399 + "node_modules/dequal": {
  2400 + "version": "2.0.3",
  2401 + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
  2402 + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
  2403 + "dev": true,
  2404 + "license": "MIT",
  2405 + "engines": {
  2406 + "node": ">=6"
  2407 + }
  2408 + },
  2409 + "node_modules/diff-sequences": {
  2410 + "version": "29.6.3",
  2411 + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
  2412 + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
  2413 + "dev": true,
  2414 + "license": "MIT",
  2415 + "engines": {
  2416 + "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
  2417 + }
  2418 + },
  2419 + "node_modules/dom-accessibility-api": {
  2420 + "version": "0.5.16",
  2421 + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
  2422 + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
  2423 + "dev": true,
  2424 + "license": "MIT",
  2425 + "peer": true
  2426 + },
  2427 + "node_modules/dunder-proto": {
  2428 + "version": "1.0.1",
  2429 + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
  2430 + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
  2431 + "license": "MIT",
  2432 + "dependencies": {
  2433 + "call-bind-apply-helpers": "^1.0.1",
  2434 + "es-errors": "^1.3.0",
  2435 + "gopd": "^1.2.0"
  2436 + },
  2437 + "engines": {
  2438 + "node": ">= 0.4"
  2439 + }
  2440 + },
  2441 + "node_modules/electron-to-chromium": {
  2442 + "version": "1.5.352",
  2443 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.352.tgz",
  2444 + "integrity": "sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg==",
  2445 + "dev": true,
  2446 + "license": "ISC"
  2447 + },
  2448 + "node_modules/entities": {
  2449 + "version": "6.0.1",
  2450 + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
  2451 + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
  2452 + "dev": true,
  2453 + "license": "BSD-2-Clause",
  2454 + "engines": {
  2455 + "node": ">=0.12"
  2456 + },
  2457 + "funding": {
  2458 + "url": "https://github.com/fb55/entities?sponsor=1"
  2459 + }
  2460 + },
  2461 + "node_modules/es-define-property": {
  2462 + "version": "1.0.1",
  2463 + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
  2464 + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
  2465 + "license": "MIT",
  2466 + "engines": {
  2467 + "node": ">= 0.4"
  2468 + }
  2469 + },
  2470 + "node_modules/es-errors": {
  2471 + "version": "1.3.0",
  2472 + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
  2473 + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
  2474 + "license": "MIT",
  2475 + "engines": {
  2476 + "node": ">= 0.4"
  2477 + }
  2478 + },
  2479 + "node_modules/es-object-atoms": {
  2480 + "version": "1.1.1",
  2481 + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
  2482 + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
  2483 + "license": "MIT",
  2484 + "dependencies": {
  2485 + "es-errors": "^1.3.0"
  2486 + },
  2487 + "engines": {
  2488 + "node": ">= 0.4"
  2489 + }
  2490 + },
  2491 + "node_modules/es-set-tostringtag": {
  2492 + "version": "2.1.0",
  2493 + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
  2494 + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
  2495 + "license": "MIT",
  2496 + "dependencies": {
  2497 + "es-errors": "^1.3.0",
  2498 + "get-intrinsic": "^1.2.6",
  2499 + "has-tostringtag": "^1.0.2",
  2500 + "hasown": "^2.0.2"
  2501 + },
  2502 + "engines": {
  2503 + "node": ">= 0.4"
  2504 + }
  2505 + },
  2506 + "node_modules/esbuild": {
  2507 + "version": "0.21.5",
  2508 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
  2509 + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
  2510 + "dev": true,
  2511 + "hasInstallScript": true,
  2512 + "license": "MIT",
  2513 + "bin": {
  2514 + "esbuild": "bin/esbuild"
  2515 + },
  2516 + "engines": {
  2517 + "node": ">=12"
  2518 + },
  2519 + "optionalDependencies": {
  2520 + "@esbuild/aix-ppc64": "0.21.5",
  2521 + "@esbuild/android-arm": "0.21.5",
  2522 + "@esbuild/android-arm64": "0.21.5",
  2523 + "@esbuild/android-x64": "0.21.5",
  2524 + "@esbuild/darwin-arm64": "0.21.5",
  2525 + "@esbuild/darwin-x64": "0.21.5",
  2526 + "@esbuild/freebsd-arm64": "0.21.5",
  2527 + "@esbuild/freebsd-x64": "0.21.5",
  2528 + "@esbuild/linux-arm": "0.21.5",
  2529 + "@esbuild/linux-arm64": "0.21.5",
  2530 + "@esbuild/linux-ia32": "0.21.5",
  2531 + "@esbuild/linux-loong64": "0.21.5",
  2532 + "@esbuild/linux-mips64el": "0.21.5",
  2533 + "@esbuild/linux-ppc64": "0.21.5",
  2534 + "@esbuild/linux-riscv64": "0.21.5",
  2535 + "@esbuild/linux-s390x": "0.21.5",
  2536 + "@esbuild/linux-x64": "0.21.5",
  2537 + "@esbuild/netbsd-x64": "0.21.5",
  2538 + "@esbuild/openbsd-x64": "0.21.5",
  2539 + "@esbuild/sunos-x64": "0.21.5",
  2540 + "@esbuild/win32-arm64": "0.21.5",
  2541 + "@esbuild/win32-ia32": "0.21.5",
  2542 + "@esbuild/win32-x64": "0.21.5"
  2543 + }
  2544 + },
  2545 + "node_modules/escalade": {
  2546 + "version": "3.2.0",
  2547 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
  2548 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
  2549 + "dev": true,
  2550 + "license": "MIT",
  2551 + "engines": {
  2552 + "node": ">=6"
  2553 + }
  2554 + },
  2555 + "node_modules/estree-walker": {
  2556 + "version": "3.0.3",
  2557 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
  2558 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
  2559 + "dev": true,
  2560 + "license": "MIT",
  2561 + "dependencies": {
  2562 + "@types/estree": "^1.0.0"
  2563 + }
  2564 + },
  2565 + "node_modules/execa": {
  2566 + "version": "8.0.1",
  2567 + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
  2568 + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
  2569 + "dev": true,
  2570 + "license": "MIT",
  2571 + "dependencies": {
  2572 + "cross-spawn": "^7.0.3",
  2573 + "get-stream": "^8.0.1",
  2574 + "human-signals": "^5.0.0",
  2575 + "is-stream": "^3.0.0",
  2576 + "merge-stream": "^2.0.0",
  2577 + "npm-run-path": "^5.1.0",
  2578 + "onetime": "^6.0.0",
  2579 + "signal-exit": "^4.1.0",
  2580 + "strip-final-newline": "^3.0.0"
  2581 + },
  2582 + "engines": {
  2583 + "node": ">=16.17"
  2584 + },
  2585 + "funding": {
  2586 + "url": "https://github.com/sindresorhus/execa?sponsor=1"
  2587 + }
  2588 + },
  2589 + "node_modules/follow-redirects": {
  2590 + "version": "1.16.0",
  2591 + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
  2592 + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
  2593 + "funding": [
  2594 + {
  2595 + "type": "individual",
  2596 + "url": "https://github.com/sponsors/RubenVerborgh"
  2597 + }
  2598 + ],
  2599 + "license": "MIT",
  2600 + "engines": {
  2601 + "node": ">=4.0"
  2602 + },
  2603 + "peerDependenciesMeta": {
  2604 + "debug": {
  2605 + "optional": true
  2606 + }
  2607 + }
  2608 + },
  2609 + "node_modules/form-data": {
  2610 + "version": "4.0.5",
  2611 + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
  2612 + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
  2613 + "license": "MIT",
  2614 + "dependencies": {
  2615 + "asynckit": "^0.4.0",
  2616 + "combined-stream": "^1.0.8",
  2617 + "es-set-tostringtag": "^2.1.0",
  2618 + "hasown": "^2.0.2",
  2619 + "mime-types": "^2.1.12"
  2620 + },
  2621 + "engines": {
  2622 + "node": ">= 6"
  2623 + }
  2624 + },
  2625 + "node_modules/fsevents": {
  2626 + "version": "2.3.3",
  2627 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
  2628 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
  2629 + "dev": true,
  2630 + "hasInstallScript": true,
  2631 + "license": "MIT",
  2632 + "optional": true,
  2633 + "os": [
  2634 + "darwin"
  2635 + ],
  2636 + "engines": {
  2637 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
  2638 + }
  2639 + },
  2640 + "node_modules/function-bind": {
  2641 + "version": "1.1.2",
  2642 + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
  2643 + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
  2644 + "license": "MIT",
  2645 + "funding": {
  2646 + "url": "https://github.com/sponsors/ljharb"
  2647 + }
  2648 + },
  2649 + "node_modules/gensync": {
  2650 + "version": "1.0.0-beta.2",
  2651 + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
  2652 + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
  2653 + "dev": true,
  2654 + "license": "MIT",
  2655 + "engines": {
  2656 + "node": ">=6.9.0"
  2657 + }
  2658 + },
  2659 + "node_modules/get-func-name": {
  2660 + "version": "2.0.2",
  2661 + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
  2662 + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
  2663 + "dev": true,
  2664 + "license": "MIT",
  2665 + "engines": {
  2666 + "node": "*"
  2667 + }
  2668 + },
  2669 + "node_modules/get-intrinsic": {
  2670 + "version": "1.3.0",
  2671 + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
  2672 + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
  2673 + "license": "MIT",
  2674 + "dependencies": {
  2675 + "call-bind-apply-helpers": "^1.0.2",
  2676 + "es-define-property": "^1.0.1",
  2677 + "es-errors": "^1.3.0",
  2678 + "es-object-atoms": "^1.1.1",
  2679 + "function-bind": "^1.1.2",
  2680 + "get-proto": "^1.0.1",
  2681 + "gopd": "^1.2.0",
  2682 + "has-symbols": "^1.1.0",
  2683 + "hasown": "^2.0.2",
  2684 + "math-intrinsics": "^1.1.0"
  2685 + },
  2686 + "engines": {
  2687 + "node": ">= 0.4"
  2688 + },
  2689 + "funding": {
  2690 + "url": "https://github.com/sponsors/ljharb"
  2691 + }
  2692 + },
  2693 + "node_modules/get-proto": {
  2694 + "version": "1.0.1",
  2695 + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
  2696 + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
  2697 + "license": "MIT",
  2698 + "dependencies": {
  2699 + "dunder-proto": "^1.0.1",
  2700 + "es-object-atoms": "^1.0.0"
  2701 + },
  2702 + "engines": {
  2703 + "node": ">= 0.4"
  2704 + }
  2705 + },
  2706 + "node_modules/get-stream": {
  2707 + "version": "8.0.1",
  2708 + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
  2709 + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
  2710 + "dev": true,
  2711 + "license": "MIT",
  2712 + "engines": {
  2713 + "node": ">=16"
  2714 + },
  2715 + "funding": {
  2716 + "url": "https://github.com/sponsors/sindresorhus"
  2717 + }
  2718 + },
  2719 + "node_modules/gopd": {
  2720 + "version": "1.2.0",
  2721 + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
  2722 + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
  2723 + "license": "MIT",
  2724 + "engines": {
  2725 + "node": ">= 0.4"
  2726 + },
  2727 + "funding": {
  2728 + "url": "https://github.com/sponsors/ljharb"
  2729 + }
  2730 + },
  2731 + "node_modules/has-symbols": {
  2732 + "version": "1.1.0",
  2733 + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
  2734 + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
  2735 + "license": "MIT",
  2736 + "engines": {
  2737 + "node": ">= 0.4"
  2738 + },
  2739 + "funding": {
  2740 + "url": "https://github.com/sponsors/ljharb"
  2741 + }
  2742 + },
  2743 + "node_modules/has-tostringtag": {
  2744 + "version": "1.0.2",
  2745 + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
  2746 + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
  2747 + "license": "MIT",
  2748 + "dependencies": {
  2749 + "has-symbols": "^1.0.3"
  2750 + },
  2751 + "engines": {
  2752 + "node": ">= 0.4"
  2753 + },
  2754 + "funding": {
  2755 + "url": "https://github.com/sponsors/ljharb"
  2756 + }
  2757 + },
  2758 + "node_modules/hasown": {
  2759 + "version": "2.0.3",
  2760 + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
  2761 + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
  2762 + "license": "MIT",
  2763 + "dependencies": {
  2764 + "function-bind": "^1.1.2"
  2765 + },
  2766 + "engines": {
  2767 + "node": ">= 0.4"
  2768 + }
  2769 + },
  2770 + "node_modules/html-encoding-sniffer": {
  2771 + "version": "4.0.0",
  2772 + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
  2773 + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
  2774 + "dev": true,
  2775 + "license": "MIT",
  2776 + "dependencies": {
  2777 + "whatwg-encoding": "^3.1.1"
  2778 + },
  2779 + "engines": {
  2780 + "node": ">=18"
  2781 + }
  2782 + },
  2783 + "node_modules/http-proxy-agent": {
  2784 + "version": "7.0.2",
  2785 + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
  2786 + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
  2787 + "dev": true,
  2788 + "license": "MIT",
  2789 + "dependencies": {
  2790 + "agent-base": "^7.1.0",
  2791 + "debug": "^4.3.4"
  2792 + },
  2793 + "engines": {
  2794 + "node": ">= 14"
  2795 + }
  2796 + },
  2797 + "node_modules/https-proxy-agent": {
  2798 + "version": "7.0.6",
  2799 + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
  2800 + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
  2801 + "dev": true,
  2802 + "license": "MIT",
  2803 + "dependencies": {
  2804 + "agent-base": "^7.1.2",
  2805 + "debug": "4"
  2806 + },
  2807 + "engines": {
  2808 + "node": ">= 14"
  2809 + }
  2810 + },
  2811 + "node_modules/human-signals": {
  2812 + "version": "5.0.0",
  2813 + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
  2814 + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
  2815 + "dev": true,
  2816 + "license": "Apache-2.0",
  2817 + "engines": {
  2818 + "node": ">=16.17.0"
  2819 + }
  2820 + },
  2821 + "node_modules/iconv-lite": {
  2822 + "version": "0.6.3",
  2823 + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
  2824 + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
  2825 + "dev": true,
  2826 + "license": "MIT",
  2827 + "dependencies": {
  2828 + "safer-buffer": ">= 2.1.2 < 3.0.0"
  2829 + },
  2830 + "engines": {
  2831 + "node": ">=0.10.0"
  2832 + }
  2833 + },
  2834 + "node_modules/immer": {
  2835 + "version": "11.1.7",
  2836 + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.7.tgz",
  2837 + "integrity": "sha512-LFVFtAROHcDy1er5UI6nodRFnZ2SgdCXhfNSI+DpObO8N7Pur/muBGsjzH5wpnFHCYhYVQxZskCkV4koQ//3/Q==",
  2838 + "license": "MIT",
  2839 + "funding": {
  2840 + "type": "opencollective",
  2841 + "url": "https://opencollective.com/immer"
  2842 + }
  2843 + },
  2844 + "node_modules/indent-string": {
  2845 + "version": "4.0.0",
  2846 + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
  2847 + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
  2848 + "dev": true,
  2849 + "license": "MIT",
  2850 + "engines": {
  2851 + "node": ">=8"
  2852 + }
  2853 + },
  2854 + "node_modules/is-potential-custom-element-name": {
  2855 + "version": "1.0.1",
  2856 + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
  2857 + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
  2858 + "dev": true,
  2859 + "license": "MIT"
  2860 + },
  2861 + "node_modules/is-stream": {
  2862 + "version": "3.0.0",
  2863 + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
  2864 + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
  2865 + "dev": true,
  2866 + "license": "MIT",
  2867 + "engines": {
  2868 + "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
  2869 + },
  2870 + "funding": {
  2871 + "url": "https://github.com/sponsors/sindresorhus"
  2872 + }
  2873 + },
  2874 + "node_modules/isexe": {
  2875 + "version": "2.0.0",
  2876 + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
  2877 + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
  2878 + "dev": true,
  2879 + "license": "ISC"
  2880 + },
  2881 + "node_modules/js-tokens": {
  2882 + "version": "4.0.0",
  2883 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
  2884 + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
  2885 + "license": "MIT"
  2886 + },
  2887 + "node_modules/jsdom": {
  2888 + "version": "24.1.3",
  2889 + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz",
  2890 + "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
  2891 + "dev": true,
  2892 + "license": "MIT",
  2893 + "dependencies": {
  2894 + "cssstyle": "^4.0.1",
  2895 + "data-urls": "^5.0.0",
  2896 + "decimal.js": "^10.4.3",
  2897 + "form-data": "^4.0.0",
  2898 + "html-encoding-sniffer": "^4.0.0",
  2899 + "http-proxy-agent": "^7.0.2",
  2900 + "https-proxy-agent": "^7.0.5",
  2901 + "is-potential-custom-element-name": "^1.0.1",
  2902 + "nwsapi": "^2.2.12",
  2903 + "parse5": "^7.1.2",
  2904 + "rrweb-cssom": "^0.7.1",
  2905 + "saxes": "^6.0.0",
  2906 + "symbol-tree": "^3.2.4",
  2907 + "tough-cookie": "^4.1.4",
  2908 + "w3c-xmlserializer": "^5.0.0",
  2909 + "webidl-conversions": "^7.0.0",
  2910 + "whatwg-encoding": "^3.1.1",
  2911 + "whatwg-mimetype": "^4.0.0",
  2912 + "whatwg-url": "^14.0.0",
  2913 + "ws": "^8.18.0",
  2914 + "xml-name-validator": "^5.0.0"
  2915 + },
  2916 + "engines": {
  2917 + "node": ">=18"
  2918 + },
  2919 + "peerDependencies": {
  2920 + "canvas": "^2.11.2"
  2921 + },
  2922 + "peerDependenciesMeta": {
  2923 + "canvas": {
  2924 + "optional": true
  2925 + }
  2926 + }
  2927 + },
  2928 + "node_modules/jsesc": {
  2929 + "version": "3.1.0",
  2930 + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
  2931 + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
  2932 + "dev": true,
  2933 + "license": "MIT",
  2934 + "bin": {
  2935 + "jsesc": "bin/jsesc"
  2936 + },
  2937 + "engines": {
  2938 + "node": ">=6"
  2939 + }
  2940 + },
  2941 + "node_modules/json2mq": {
  2942 + "version": "0.2.0",
  2943 + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
  2944 + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
  2945 + "license": "MIT",
  2946 + "dependencies": {
  2947 + "string-convert": "^0.2.0"
  2948 + }
  2949 + },
  2950 + "node_modules/json5": {
  2951 + "version": "2.2.3",
  2952 + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
  2953 + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
  2954 + "dev": true,
  2955 + "license": "MIT",
  2956 + "bin": {
  2957 + "json5": "lib/cli.js"
  2958 + },
  2959 + "engines": {
  2960 + "node": ">=6"
  2961 + }
  2962 + },
  2963 + "node_modules/local-pkg": {
  2964 + "version": "0.5.1",
  2965 + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz",
  2966 + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==",
  2967 + "dev": true,
  2968 + "license": "MIT",
  2969 + "dependencies": {
  2970 + "mlly": "^1.7.3",
  2971 + "pkg-types": "^1.2.1"
  2972 + },
  2973 + "engines": {
  2974 + "node": ">=14"
  2975 + },
  2976 + "funding": {
  2977 + "url": "https://github.com/sponsors/antfu"
  2978 + }
  2979 + },
  2980 + "node_modules/loose-envify": {
  2981 + "version": "1.4.0",
  2982 + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
  2983 + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
  2984 + "license": "MIT",
  2985 + "dependencies": {
  2986 + "js-tokens": "^3.0.0 || ^4.0.0"
  2987 + },
  2988 + "bin": {
  2989 + "loose-envify": "cli.js"
  2990 + }
  2991 + },
  2992 + "node_modules/loupe": {
  2993 + "version": "2.3.7",
  2994 + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
  2995 + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
  2996 + "dev": true,
  2997 + "license": "MIT",
  2998 + "dependencies": {
  2999 + "get-func-name": "^2.0.1"
  3000 + }
  3001 + },
  3002 + "node_modules/lru-cache": {
  3003 + "version": "5.1.1",
  3004 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
  3005 + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
  3006 + "dev": true,
  3007 + "license": "ISC",
  3008 + "dependencies": {
  3009 + "yallist": "^3.0.2"
  3010 + }
  3011 + },
  3012 + "node_modules/lz-string": {
  3013 + "version": "1.5.0",
  3014 + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
  3015 + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
  3016 + "dev": true,
  3017 + "license": "MIT",
  3018 + "peer": true,
  3019 + "bin": {
  3020 + "lz-string": "bin/bin.js"
  3021 + }
  3022 + },
  3023 + "node_modules/magic-string": {
  3024 + "version": "0.30.21",
  3025 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
  3026 + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
  3027 + "dev": true,
  3028 + "license": "MIT",
  3029 + "dependencies": {
  3030 + "@jridgewell/sourcemap-codec": "^1.5.5"
  3031 + }
  3032 + },
  3033 + "node_modules/math-intrinsics": {
  3034 + "version": "1.1.0",
  3035 + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
  3036 + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
  3037 + "license": "MIT",
  3038 + "engines": {
  3039 + "node": ">= 0.4"
  3040 + }
  3041 + },
  3042 + "node_modules/merge-stream": {
  3043 + "version": "2.0.0",
  3044 + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
  3045 + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
  3046 + "dev": true,
  3047 + "license": "MIT"
  3048 + },
  3049 + "node_modules/mime-db": {
  3050 + "version": "1.52.0",
  3051 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
  3052 + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
  3053 + "license": "MIT",
  3054 + "engines": {
  3055 + "node": ">= 0.6"
  3056 + }
  3057 + },
  3058 + "node_modules/mime-types": {
  3059 + "version": "2.1.35",
  3060 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
  3061 + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
  3062 + "license": "MIT",
  3063 + "dependencies": {
  3064 + "mime-db": "1.52.0"
  3065 + },
  3066 + "engines": {
  3067 + "node": ">= 0.6"
  3068 + }
  3069 + },
  3070 + "node_modules/mimic-fn": {
  3071 + "version": "4.0.0",
  3072 + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
  3073 + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
  3074 + "dev": true,
  3075 + "license": "MIT",
  3076 + "engines": {
  3077 + "node": ">=12"
  3078 + },
  3079 + "funding": {
  3080 + "url": "https://github.com/sponsors/sindresorhus"
  3081 + }
  3082 + },
  3083 + "node_modules/min-indent": {
  3084 + "version": "1.0.1",
  3085 + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
  3086 + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
  3087 + "dev": true,
  3088 + "license": "MIT",
  3089 + "engines": {
  3090 + "node": ">=4"
  3091 + }
  3092 + },
  3093 + "node_modules/mlly": {
  3094 + "version": "1.8.2",
  3095 + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
  3096 + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
  3097 + "dev": true,
  3098 + "license": "MIT",
  3099 + "dependencies": {
  3100 + "acorn": "^8.16.0",
  3101 + "pathe": "^2.0.3",
  3102 + "pkg-types": "^1.3.1",
  3103 + "ufo": "^1.6.3"
  3104 + }
  3105 + },
  3106 + "node_modules/mlly/node_modules/pathe": {
  3107 + "version": "2.0.3",
  3108 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
  3109 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
  3110 + "dev": true,
  3111 + "license": "MIT"
  3112 + },
  3113 + "node_modules/ms": {
  3114 + "version": "2.1.3",
  3115 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
  3116 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
  3117 + "dev": true,
  3118 + "license": "MIT"
  3119 + },
  3120 + "node_modules/nanoid": {
  3121 + "version": "3.3.12",
  3122 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
  3123 + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
  3124 + "dev": true,
  3125 + "funding": [
  3126 + {
  3127 + "type": "github",
  3128 + "url": "https://github.com/sponsors/ai"
  3129 + }
  3130 + ],
  3131 + "license": "MIT",
  3132 + "bin": {
  3133 + "nanoid": "bin/nanoid.cjs"
  3134 + },
  3135 + "engines": {
  3136 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
  3137 + }
  3138 + },
  3139 + "node_modules/node-releases": {
  3140 + "version": "2.0.38",
  3141 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
  3142 + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
  3143 + "dev": true,
  3144 + "license": "MIT"
  3145 + },
  3146 + "node_modules/npm-run-path": {
  3147 + "version": "5.3.0",
  3148 + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
  3149 + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
  3150 + "dev": true,
  3151 + "license": "MIT",
  3152 + "dependencies": {
  3153 + "path-key": "^4.0.0"
  3154 + },
  3155 + "engines": {
  3156 + "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
  3157 + },
  3158 + "funding": {
  3159 + "url": "https://github.com/sponsors/sindresorhus"
  3160 + }
  3161 + },
  3162 + "node_modules/npm-run-path/node_modules/path-key": {
  3163 + "version": "4.0.0",
  3164 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
  3165 + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
  3166 + "dev": true,
  3167 + "license": "MIT",
  3168 + "engines": {
  3169 + "node": ">=12"
  3170 + },
  3171 + "funding": {
  3172 + "url": "https://github.com/sponsors/sindresorhus"
  3173 + }
  3174 + },
  3175 + "node_modules/nwsapi": {
  3176 + "version": "2.2.23",
  3177 + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
  3178 + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
  3179 + "dev": true,
  3180 + "license": "MIT"
  3181 + },
  3182 + "node_modules/onetime": {
  3183 + "version": "6.0.0",
  3184 + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
  3185 + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
  3186 + "dev": true,
  3187 + "license": "MIT",
  3188 + "dependencies": {
  3189 + "mimic-fn": "^4.0.0"
  3190 + },
  3191 + "engines": {
  3192 + "node": ">=12"
  3193 + },
  3194 + "funding": {
  3195 + "url": "https://github.com/sponsors/sindresorhus"
  3196 + }
  3197 + },
  3198 + "node_modules/p-limit": {
  3199 + "version": "5.0.0",
  3200 + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz",
  3201 + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==",
  3202 + "dev": true,
  3203 + "license": "MIT",
  3204 + "dependencies": {
  3205 + "yocto-queue": "^1.0.0"
  3206 + },
  3207 + "engines": {
  3208 + "node": ">=18"
  3209 + },
  3210 + "funding": {
  3211 + "url": "https://github.com/sponsors/sindresorhus"
  3212 + }
  3213 + },
  3214 + "node_modules/parse5": {
  3215 + "version": "7.3.0",
  3216 + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
  3217 + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
  3218 + "dev": true,
  3219 + "license": "MIT",
  3220 + "dependencies": {
  3221 + "entities": "^6.0.0"
  3222 + },
  3223 + "funding": {
  3224 + "url": "https://github.com/inikulin/parse5?sponsor=1"
  3225 + }
  3226 + },
  3227 + "node_modules/path-key": {
  3228 + "version": "3.1.1",
  3229 + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
  3230 + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
  3231 + "dev": true,
  3232 + "license": "MIT",
  3233 + "engines": {
  3234 + "node": ">=8"
  3235 + }
  3236 + },
  3237 + "node_modules/pathe": {
  3238 + "version": "1.1.2",
  3239 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
  3240 + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
  3241 + "dev": true,
  3242 + "license": "MIT"
  3243 + },
  3244 + "node_modules/pathval": {
  3245 + "version": "1.1.1",
  3246 + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
  3247 + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
  3248 + "dev": true,
  3249 + "license": "MIT",
  3250 + "engines": {
  3251 + "node": "*"
  3252 + }
  3253 + },
  3254 + "node_modules/picocolors": {
  3255 + "version": "1.1.1",
  3256 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
  3257 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
  3258 + "dev": true,
  3259 + "license": "ISC"
  3260 + },
  3261 + "node_modules/pkg-types": {
  3262 + "version": "1.3.1",
  3263 + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
  3264 + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
  3265 + "dev": true,
  3266 + "license": "MIT",
  3267 + "dependencies": {
  3268 + "confbox": "^0.1.8",
  3269 + "mlly": "^1.7.4",
  3270 + "pathe": "^2.0.1"
  3271 + }
  3272 + },
  3273 + "node_modules/pkg-types/node_modules/pathe": {
  3274 + "version": "2.0.3",
  3275 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
  3276 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
  3277 + "dev": true,
  3278 + "license": "MIT"
  3279 + },
  3280 + "node_modules/postcss": {
  3281 + "version": "8.5.14",
  3282 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
  3283 + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
  3284 + "dev": true,
  3285 + "funding": [
  3286 + {
  3287 + "type": "opencollective",
  3288 + "url": "https://opencollective.com/postcss/"
  3289 + },
  3290 + {
  3291 + "type": "tidelift",
  3292 + "url": "https://tidelift.com/funding/github/npm/postcss"
  3293 + },
  3294 + {
  3295 + "type": "github",
  3296 + "url": "https://github.com/sponsors/ai"
  3297 + }
  3298 + ],
  3299 + "license": "MIT",
  3300 + "dependencies": {
  3301 + "nanoid": "^3.3.11",
  3302 + "picocolors": "^1.1.1",
  3303 + "source-map-js": "^1.2.1"
  3304 + },
  3305 + "engines": {
  3306 + "node": "^10 || ^12 || >=14"
  3307 + }
  3308 + },
  3309 + "node_modules/pretty-format": {
  3310 + "version": "27.5.1",
  3311 + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
  3312 + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
  3313 + "dev": true,
  3314 + "license": "MIT",
  3315 + "peer": true,
  3316 + "dependencies": {
  3317 + "ansi-regex": "^5.0.1",
  3318 + "ansi-styles": "^5.0.0",
  3319 + "react-is": "^17.0.1"
  3320 + },
  3321 + "engines": {
  3322 + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
  3323 + }
  3324 + },
  3325 + "node_modules/proxy-from-env": {
  3326 + "version": "2.1.0",
  3327 + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
  3328 + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
  3329 + "license": "MIT",
  3330 + "engines": {
  3331 + "node": ">=10"
  3332 + }
  3333 + },
  3334 + "node_modules/psl": {
  3335 + "version": "1.15.0",
  3336 + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
  3337 + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
  3338 + "dev": true,
  3339 + "license": "MIT",
  3340 + "dependencies": {
  3341 + "punycode": "^2.3.1"
  3342 + },
  3343 + "funding": {
  3344 + "url": "https://github.com/sponsors/lupomontero"
  3345 + }
  3346 + },
  3347 + "node_modules/punycode": {
  3348 + "version": "2.3.1",
  3349 + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
  3350 + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
  3351 + "dev": true,
  3352 + "license": "MIT",
  3353 + "engines": {
  3354 + "node": ">=6"
  3355 + }
  3356 + },
  3357 + "node_modules/querystringify": {
  3358 + "version": "2.2.0",
  3359 + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
  3360 + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
  3361 + "dev": true,
  3362 + "license": "MIT"
  3363 + },
  3364 + "node_modules/rc-cascader": {
  3365 + "version": "3.34.0",
  3366 + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz",
  3367 + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==",
  3368 + "license": "MIT",
  3369 + "dependencies": {
  3370 + "@babel/runtime": "^7.25.7",
  3371 + "classnames": "^2.3.1",
  3372 + "rc-select": "~14.16.2",
  3373 + "rc-tree": "~5.13.0",
  3374 + "rc-util": "^5.43.0"
  3375 + },
  3376 + "peerDependencies": {
  3377 + "react": ">=16.9.0",
  3378 + "react-dom": ">=16.9.0"
  3379 + }
  3380 + },
  3381 + "node_modules/rc-checkbox": {
  3382 + "version": "3.5.0",
  3383 + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz",
  3384 + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==",
  3385 + "license": "MIT",
  3386 + "dependencies": {
  3387 + "@babel/runtime": "^7.10.1",
  3388 + "classnames": "^2.3.2",
  3389 + "rc-util": "^5.25.2"
  3390 + },
  3391 + "peerDependencies": {
  3392 + "react": ">=16.9.0",
  3393 + "react-dom": ">=16.9.0"
  3394 + }
  3395 + },
  3396 + "node_modules/rc-collapse": {
  3397 + "version": "3.9.0",
  3398 + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz",
  3399 + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==",
  3400 + "license": "MIT",
  3401 + "dependencies": {
  3402 + "@babel/runtime": "^7.10.1",
  3403 + "classnames": "2.x",
  3404 + "rc-motion": "^2.3.4",
  3405 + "rc-util": "^5.27.0"
  3406 + },
  3407 + "peerDependencies": {
  3408 + "react": ">=16.9.0",
  3409 + "react-dom": ">=16.9.0"
  3410 + }
  3411 + },
  3412 + "node_modules/rc-dialog": {
  3413 + "version": "9.6.0",
  3414 + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz",
  3415 + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==",
  3416 + "license": "MIT",
  3417 + "dependencies": {
  3418 + "@babel/runtime": "^7.10.1",
  3419 + "@rc-component/portal": "^1.0.0-8",
  3420 + "classnames": "^2.2.6",
  3421 + "rc-motion": "^2.3.0",
  3422 + "rc-util": "^5.21.0"
  3423 + },
  3424 + "peerDependencies": {
  3425 + "react": ">=16.9.0",
  3426 + "react-dom": ">=16.9.0"
  3427 + }
  3428 + },
  3429 + "node_modules/rc-drawer": {
  3430 + "version": "7.3.0",
  3431 + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz",
  3432 + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==",
  3433 + "license": "MIT",
  3434 + "dependencies": {
  3435 + "@babel/runtime": "^7.23.9",
  3436 + "@rc-component/portal": "^1.1.1",
  3437 + "classnames": "^2.2.6",
  3438 + "rc-motion": "^2.6.1",
  3439 + "rc-util": "^5.38.1"
  3440 + },
  3441 + "peerDependencies": {
  3442 + "react": ">=16.9.0",
  3443 + "react-dom": ">=16.9.0"
  3444 + }
  3445 + },
  3446 + "node_modules/rc-dropdown": {
  3447 + "version": "4.2.1",
  3448 + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz",
  3449 + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==",
  3450 + "license": "MIT",
  3451 + "dependencies": {
  3452 + "@babel/runtime": "^7.18.3",
  3453 + "@rc-component/trigger": "^2.0.0",
  3454 + "classnames": "^2.2.6",
  3455 + "rc-util": "^5.44.1"
  3456 + },
  3457 + "peerDependencies": {
  3458 + "react": ">=16.11.0",
  3459 + "react-dom": ">=16.11.0"
  3460 + }
  3461 + },
  3462 + "node_modules/rc-field-form": {
  3463 + "version": "2.7.1",
  3464 + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.1.tgz",
  3465 + "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==",
  3466 + "license": "MIT",
  3467 + "dependencies": {
  3468 + "@babel/runtime": "^7.18.0",
  3469 + "@rc-component/async-validator": "^5.0.3",
  3470 + "rc-util": "^5.32.2"
  3471 + },
  3472 + "engines": {
  3473 + "node": ">=8.x"
  3474 + },
  3475 + "peerDependencies": {
  3476 + "react": ">=16.9.0",
  3477 + "react-dom": ">=16.9.0"
  3478 + }
  3479 + },
  3480 + "node_modules/rc-image": {
  3481 + "version": "7.12.0",
  3482 + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz",
  3483 + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==",
  3484 + "license": "MIT",
  3485 + "dependencies": {
  3486 + "@babel/runtime": "^7.11.2",
  3487 + "@rc-component/portal": "^1.0.2",
  3488 + "classnames": "^2.2.6",
  3489 + "rc-dialog": "~9.6.0",
  3490 + "rc-motion": "^2.6.2",
  3491 + "rc-util": "^5.34.1"
  3492 + },
  3493 + "peerDependencies": {
  3494 + "react": ">=16.9.0",
  3495 + "react-dom": ">=16.9.0"
  3496 + }
  3497 + },
  3498 + "node_modules/rc-input": {
  3499 + "version": "1.8.0",
  3500 + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz",
  3501 + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==",
  3502 + "license": "MIT",
  3503 + "dependencies": {
  3504 + "@babel/runtime": "^7.11.1",
  3505 + "classnames": "^2.2.1",
  3506 + "rc-util": "^5.18.1"
  3507 + },
  3508 + "peerDependencies": {
  3509 + "react": ">=16.0.0",
  3510 + "react-dom": ">=16.0.0"
  3511 + }
  3512 + },
  3513 + "node_modules/rc-input-number": {
  3514 + "version": "9.5.0",
  3515 + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz",
  3516 + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==",
  3517 + "license": "MIT",
  3518 + "dependencies": {
  3519 + "@babel/runtime": "^7.10.1",
  3520 + "@rc-component/mini-decimal": "^1.0.1",
  3521 + "classnames": "^2.2.5",
  3522 + "rc-input": "~1.8.0",
  3523 + "rc-util": "^5.40.1"
  3524 + },
  3525 + "peerDependencies": {
  3526 + "react": ">=16.9.0",
  3527 + "react-dom": ">=16.9.0"
  3528 + }
  3529 + },
  3530 + "node_modules/rc-mentions": {
  3531 + "version": "2.20.0",
  3532 + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz",
  3533 + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==",
  3534 + "license": "MIT",
  3535 + "dependencies": {
  3536 + "@babel/runtime": "^7.22.5",
  3537 + "@rc-component/trigger": "^2.0.0",
  3538 + "classnames": "^2.2.6",
  3539 + "rc-input": "~1.8.0",
  3540 + "rc-menu": "~9.16.0",
  3541 + "rc-textarea": "~1.10.0",
  3542 + "rc-util": "^5.34.1"
  3543 + },
  3544 + "peerDependencies": {
  3545 + "react": ">=16.9.0",
  3546 + "react-dom": ">=16.9.0"
  3547 + }
  3548 + },
  3549 + "node_modules/rc-menu": {
  3550 + "version": "9.16.1",
  3551 + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz",
  3552 + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==",
  3553 + "license": "MIT",
  3554 + "dependencies": {
  3555 + "@babel/runtime": "^7.10.1",
  3556 + "@rc-component/trigger": "^2.0.0",
  3557 + "classnames": "2.x",
  3558 + "rc-motion": "^2.4.3",
  3559 + "rc-overflow": "^1.3.1",
  3560 + "rc-util": "^5.27.0"
  3561 + },
  3562 + "peerDependencies": {
  3563 + "react": ">=16.9.0",
  3564 + "react-dom": ">=16.9.0"
  3565 + }
  3566 + },
  3567 + "node_modules/rc-motion": {
  3568 + "version": "2.9.5",
  3569 + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz",
  3570 + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==",
  3571 + "license": "MIT",
  3572 + "dependencies": {
  3573 + "@babel/runtime": "^7.11.1",
  3574 + "classnames": "^2.2.1",
  3575 + "rc-util": "^5.44.0"
  3576 + },
  3577 + "peerDependencies": {
  3578 + "react": ">=16.9.0",
  3579 + "react-dom": ">=16.9.0"
  3580 + }
  3581 + },
  3582 + "node_modules/rc-notification": {
  3583 + "version": "5.6.4",
  3584 + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz",
  3585 + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==",
  3586 + "license": "MIT",
  3587 + "dependencies": {
  3588 + "@babel/runtime": "^7.10.1",
  3589 + "classnames": "2.x",
  3590 + "rc-motion": "^2.9.0",
  3591 + "rc-util": "^5.20.1"
  3592 + },
  3593 + "engines": {
  3594 + "node": ">=8.x"
  3595 + },
  3596 + "peerDependencies": {
  3597 + "react": ">=16.9.0",
  3598 + "react-dom": ">=16.9.0"
  3599 + }
  3600 + },
  3601 + "node_modules/rc-overflow": {
  3602 + "version": "1.5.0",
  3603 + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.5.0.tgz",
  3604 + "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==",
  3605 + "license": "MIT",
  3606 + "dependencies": {
  3607 + "@babel/runtime": "^7.11.1",
  3608 + "classnames": "^2.2.1",
  3609 + "rc-resize-observer": "^1.0.0",
  3610 + "rc-util": "^5.37.0"
  3611 + },
  3612 + "peerDependencies": {
  3613 + "react": ">=16.9.0",
  3614 + "react-dom": ">=16.9.0"
  3615 + }
  3616 + },
  3617 + "node_modules/rc-pagination": {
  3618 + "version": "5.1.0",
  3619 + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz",
  3620 + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==",
  3621 + "license": "MIT",
  3622 + "dependencies": {
  3623 + "@babel/runtime": "^7.10.1",
  3624 + "classnames": "^2.3.2",
  3625 + "rc-util": "^5.38.0"
  3626 + },
  3627 + "peerDependencies": {
  3628 + "react": ">=16.9.0",
  3629 + "react-dom": ">=16.9.0"
  3630 + }
  3631 + },
  3632 + "node_modules/rc-picker": {
  3633 + "version": "4.11.3",
  3634 + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz",
  3635 + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==",
  3636 + "license": "MIT",
  3637 + "dependencies": {
  3638 + "@babel/runtime": "^7.24.7",
  3639 + "@rc-component/trigger": "^2.0.0",
  3640 + "classnames": "^2.2.1",
  3641 + "rc-overflow": "^1.3.2",
  3642 + "rc-resize-observer": "^1.4.0",
  3643 + "rc-util": "^5.43.0"
  3644 + },
  3645 + "engines": {
  3646 + "node": ">=8.x"
  3647 + },
  3648 + "peerDependencies": {
  3649 + "date-fns": ">= 2.x",
  3650 + "dayjs": ">= 1.x",
  3651 + "luxon": ">= 3.x",
  3652 + "moment": ">= 2.x",
  3653 + "react": ">=16.9.0",
  3654 + "react-dom": ">=16.9.0"
  3655 + },
  3656 + "peerDependenciesMeta": {
  3657 + "date-fns": {
  3658 + "optional": true
  3659 + },
  3660 + "dayjs": {
  3661 + "optional": true
  3662 + },
  3663 + "luxon": {
  3664 + "optional": true
  3665 + },
  3666 + "moment": {
  3667 + "optional": true
  3668 + }
  3669 + }
  3670 + },
  3671 + "node_modules/rc-progress": {
  3672 + "version": "4.0.0",
  3673 + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz",
  3674 + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==",
  3675 + "license": "MIT",
  3676 + "dependencies": {
  3677 + "@babel/runtime": "^7.10.1",
  3678 + "classnames": "^2.2.6",
  3679 + "rc-util": "^5.16.1"
  3680 + },
  3681 + "peerDependencies": {
  3682 + "react": ">=16.9.0",
  3683 + "react-dom": ">=16.9.0"
  3684 + }
  3685 + },
  3686 + "node_modules/rc-rate": {
  3687 + "version": "2.13.1",
  3688 + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz",
  3689 + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==",
  3690 + "license": "MIT",
  3691 + "dependencies": {
  3692 + "@babel/runtime": "^7.10.1",
  3693 + "classnames": "^2.2.5",
  3694 + "rc-util": "^5.0.1"
  3695 + },
  3696 + "engines": {
  3697 + "node": ">=8.x"
  3698 + },
  3699 + "peerDependencies": {
  3700 + "react": ">=16.9.0",
  3701 + "react-dom": ">=16.9.0"
  3702 + }
  3703 + },
  3704 + "node_modules/rc-resize-observer": {
  3705 + "version": "1.4.3",
  3706 + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz",
  3707 + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==",
  3708 + "license": "MIT",
  3709 + "dependencies": {
  3710 + "@babel/runtime": "^7.20.7",
  3711 + "classnames": "^2.2.1",
  3712 + "rc-util": "^5.44.1",
  3713 + "resize-observer-polyfill": "^1.5.1"
  3714 + },
  3715 + "peerDependencies": {
  3716 + "react": ">=16.9.0",
  3717 + "react-dom": ">=16.9.0"
  3718 + }
  3719 + },
  3720 + "node_modules/rc-segmented": {
  3721 + "version": "2.7.1",
  3722 + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.1.tgz",
  3723 + "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==",
  3724 + "license": "MIT",
  3725 + "dependencies": {
  3726 + "@babel/runtime": "^7.11.1",
  3727 + "classnames": "^2.2.1",
  3728 + "rc-motion": "^2.4.4",
  3729 + "rc-util": "^5.17.0"
  3730 + },
  3731 + "peerDependencies": {
  3732 + "react": ">=16.0.0",
  3733 + "react-dom": ">=16.0.0"
  3734 + }
  3735 + },
  3736 + "node_modules/rc-select": {
  3737 + "version": "14.16.8",
  3738 + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz",
  3739 + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==",
  3740 + "license": "MIT",
  3741 + "dependencies": {
  3742 + "@babel/runtime": "^7.10.1",
  3743 + "@rc-component/trigger": "^2.1.1",
  3744 + "classnames": "2.x",
  3745 + "rc-motion": "^2.0.1",
  3746 + "rc-overflow": "^1.3.1",
  3747 + "rc-util": "^5.16.1",
  3748 + "rc-virtual-list": "^3.5.2"
  3749 + },
  3750 + "engines": {
  3751 + "node": ">=8.x"
  3752 + },
  3753 + "peerDependencies": {
  3754 + "react": "*",
  3755 + "react-dom": "*"
  3756 + }
  3757 + },
  3758 + "node_modules/rc-slider": {
  3759 + "version": "11.1.9",
  3760 + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz",
  3761 + "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==",
  3762 + "license": "MIT",
  3763 + "dependencies": {
  3764 + "@babel/runtime": "^7.10.1",
  3765 + "classnames": "^2.2.5",
  3766 + "rc-util": "^5.36.0"
  3767 + },
  3768 + "engines": {
  3769 + "node": ">=8.x"
  3770 + },
  3771 + "peerDependencies": {
  3772 + "react": ">=16.9.0",
  3773 + "react-dom": ">=16.9.0"
  3774 + }
  3775 + },
  3776 + "node_modules/rc-steps": {
  3777 + "version": "6.0.1",
  3778 + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz",
  3779 + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==",
  3780 + "license": "MIT",
  3781 + "dependencies": {
  3782 + "@babel/runtime": "^7.16.7",
  3783 + "classnames": "^2.2.3",
  3784 + "rc-util": "^5.16.1"
  3785 + },
  3786 + "engines": {
  3787 + "node": ">=8.x"
  3788 + },
  3789 + "peerDependencies": {
  3790 + "react": ">=16.9.0",
  3791 + "react-dom": ">=16.9.0"
  3792 + }
  3793 + },
  3794 + "node_modules/rc-switch": {
  3795 + "version": "4.1.0",
  3796 + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz",
  3797 + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==",
  3798 + "license": "MIT",
  3799 + "dependencies": {
  3800 + "@babel/runtime": "^7.21.0",
  3801 + "classnames": "^2.2.1",
  3802 + "rc-util": "^5.30.0"
  3803 + },
  3804 + "peerDependencies": {
  3805 + "react": ">=16.9.0",
  3806 + "react-dom": ">=16.9.0"
  3807 + }
  3808 + },
  3809 + "node_modules/rc-table": {
  3810 + "version": "7.54.0",
  3811 + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.54.0.tgz",
  3812 + "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==",
  3813 + "license": "MIT",
  3814 + "dependencies": {
  3815 + "@babel/runtime": "^7.10.1",
  3816 + "@rc-component/context": "^1.4.0",
  3817 + "classnames": "^2.2.5",
  3818 + "rc-resize-observer": "^1.1.0",
  3819 + "rc-util": "^5.44.3",
  3820 + "rc-virtual-list": "^3.14.2"
  3821 + },
  3822 + "engines": {
  3823 + "node": ">=8.x"
  3824 + },
  3825 + "peerDependencies": {
  3826 + "react": ">=16.9.0",
  3827 + "react-dom": ">=16.9.0"
  3828 + }
  3829 + },
  3830 + "node_modules/rc-tabs": {
  3831 + "version": "15.7.0",
  3832 + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz",
  3833 + "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==",
  3834 + "license": "MIT",
  3835 + "dependencies": {
  3836 + "@babel/runtime": "^7.11.2",
  3837 + "classnames": "2.x",
  3838 + "rc-dropdown": "~4.2.0",
  3839 + "rc-menu": "~9.16.0",
  3840 + "rc-motion": "^2.6.2",
  3841 + "rc-resize-observer": "^1.0.0",
  3842 + "rc-util": "^5.34.1"
  3843 + },
  3844 + "engines": {
  3845 + "node": ">=8.x"
  3846 + },
  3847 + "peerDependencies": {
  3848 + "react": ">=16.9.0",
  3849 + "react-dom": ">=16.9.0"
  3850 + }
  3851 + },
  3852 + "node_modules/rc-textarea": {
  3853 + "version": "1.10.2",
  3854 + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz",
  3855 + "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==",
  3856 + "license": "MIT",
  3857 + "dependencies": {
  3858 + "@babel/runtime": "^7.10.1",
  3859 + "classnames": "^2.2.1",
  3860 + "rc-input": "~1.8.0",
  3861 + "rc-resize-observer": "^1.0.0",
  3862 + "rc-util": "^5.27.0"
  3863 + },
  3864 + "peerDependencies": {
  3865 + "react": ">=16.9.0",
  3866 + "react-dom": ">=16.9.0"
  3867 + }
  3868 + },
  3869 + "node_modules/rc-tooltip": {
  3870 + "version": "6.4.0",
  3871 + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz",
  3872 + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==",
  3873 + "license": "MIT",
  3874 + "dependencies": {
  3875 + "@babel/runtime": "^7.11.2",
  3876 + "@rc-component/trigger": "^2.0.0",
  3877 + "classnames": "^2.3.1",
  3878 + "rc-util": "^5.44.3"
  3879 + },
  3880 + "peerDependencies": {
  3881 + "react": ">=16.9.0",
  3882 + "react-dom": ">=16.9.0"
  3883 + }
  3884 + },
  3885 + "node_modules/rc-tree": {
  3886 + "version": "5.13.1",
  3887 + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz",
  3888 + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==",
  3889 + "license": "MIT",
  3890 + "dependencies": {
  3891 + "@babel/runtime": "^7.10.1",
  3892 + "classnames": "2.x",
  3893 + "rc-motion": "^2.0.1",
  3894 + "rc-util": "^5.16.1",
  3895 + "rc-virtual-list": "^3.5.1"
  3896 + },
  3897 + "engines": {
  3898 + "node": ">=10.x"
  3899 + },
  3900 + "peerDependencies": {
  3901 + "react": "*",
  3902 + "react-dom": "*"
  3903 + }
  3904 + },
  3905 + "node_modules/rc-tree-select": {
  3906 + "version": "5.27.0",
  3907 + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz",
  3908 + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==",
  3909 + "license": "MIT",
  3910 + "dependencies": {
  3911 + "@babel/runtime": "^7.25.7",
  3912 + "classnames": "2.x",
  3913 + "rc-select": "~14.16.2",
  3914 + "rc-tree": "~5.13.0",
  3915 + "rc-util": "^5.43.0"
  3916 + },
  3917 + "peerDependencies": {
  3918 + "react": "*",
  3919 + "react-dom": "*"
  3920 + }
  3921 + },
  3922 + "node_modules/rc-upload": {
  3923 + "version": "4.11.0",
  3924 + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.11.0.tgz",
  3925 + "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==",
  3926 + "license": "MIT",
  3927 + "dependencies": {
  3928 + "@babel/runtime": "^7.18.3",
  3929 + "classnames": "^2.2.5",
  3930 + "rc-util": "^5.2.0"
  3931 + },
  3932 + "peerDependencies": {
  3933 + "react": ">=16.9.0",
  3934 + "react-dom": ">=16.9.0"
  3935 + }
  3936 + },
  3937 + "node_modules/rc-util": {
  3938 + "version": "5.44.4",
  3939 + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz",
  3940 + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==",
  3941 + "license": "MIT",
  3942 + "dependencies": {
  3943 + "@babel/runtime": "^7.18.3",
  3944 + "react-is": "^18.2.0"
  3945 + },
  3946 + "peerDependencies": {
  3947 + "react": ">=16.9.0",
  3948 + "react-dom": ">=16.9.0"
  3949 + }
  3950 + },
  3951 + "node_modules/rc-util/node_modules/react-is": {
  3952 + "version": "18.3.1",
  3953 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
  3954 + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
  3955 + "license": "MIT"
  3956 + },
  3957 + "node_modules/rc-virtual-list": {
  3958 + "version": "3.19.2",
  3959 + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz",
  3960 + "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==",
  3961 + "license": "MIT",
  3962 + "dependencies": {
  3963 + "@babel/runtime": "^7.20.0",
  3964 + "classnames": "^2.2.6",
  3965 + "rc-resize-observer": "^1.0.0",
  3966 + "rc-util": "^5.36.0"
  3967 + },
  3968 + "engines": {
  3969 + "node": ">=8.x"
  3970 + },
  3971 + "peerDependencies": {
  3972 + "react": ">=16.9.0",
  3973 + "react-dom": ">=16.9.0"
  3974 + }
  3975 + },
  3976 + "node_modules/react": {
  3977 + "version": "18.3.1",
  3978 + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
  3979 + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
  3980 + "license": "MIT",
  3981 + "dependencies": {
  3982 + "loose-envify": "^1.1.0"
  3983 + },
  3984 + "engines": {
  3985 + "node": ">=0.10.0"
  3986 + }
  3987 + },
  3988 + "node_modules/react-dom": {
  3989 + "version": "18.3.1",
  3990 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
  3991 + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
  3992 + "license": "MIT",
  3993 + "dependencies": {
  3994 + "loose-envify": "^1.1.0",
  3995 + "scheduler": "^0.23.2"
  3996 + },
  3997 + "peerDependencies": {
  3998 + "react": "^18.3.1"
  3999 + }
  4000 + },
  4001 + "node_modules/react-is": {
  4002 + "version": "17.0.2",
  4003 + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
  4004 + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
  4005 + "dev": true,
  4006 + "license": "MIT",
  4007 + "peer": true
  4008 + },
  4009 + "node_modules/react-redux": {
  4010 + "version": "9.2.0",
  4011 + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
  4012 + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
  4013 + "license": "MIT",
  4014 + "dependencies": {
  4015 + "@types/use-sync-external-store": "^0.0.6",
  4016 + "use-sync-external-store": "^1.4.0"
  4017 + },
  4018 + "peerDependencies": {
  4019 + "@types/react": "^18.2.25 || ^19",
  4020 + "react": "^18.0 || ^19",
  4021 + "redux": "^5.0.0"
  4022 + },
  4023 + "peerDependenciesMeta": {
  4024 + "@types/react": {
  4025 + "optional": true
  4026 + },
  4027 + "redux": {
  4028 + "optional": true
  4029 + }
  4030 + }
  4031 + },
  4032 + "node_modules/react-refresh": {
  4033 + "version": "0.17.0",
  4034 + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
  4035 + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
  4036 + "dev": true,
  4037 + "license": "MIT",
  4038 + "engines": {
  4039 + "node": ">=0.10.0"
  4040 + }
  4041 + },
  4042 + "node_modules/react-router": {
  4043 + "version": "6.30.3",
  4044 + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
  4045 + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
  4046 + "license": "MIT",
  4047 + "dependencies": {
  4048 + "@remix-run/router": "1.23.2"
  4049 + },
  4050 + "engines": {
  4051 + "node": ">=14.0.0"
  4052 + },
  4053 + "peerDependencies": {
  4054 + "react": ">=16.8"
  4055 + }
  4056 + },
  4057 + "node_modules/react-router-dom": {
  4058 + "version": "6.30.3",
  4059 + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
  4060 + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
  4061 + "license": "MIT",
  4062 + "dependencies": {
  4063 + "@remix-run/router": "1.23.2",
  4064 + "react-router": "6.30.3"
  4065 + },
  4066 + "engines": {
  4067 + "node": ">=14.0.0"
  4068 + },
  4069 + "peerDependencies": {
  4070 + "react": ">=16.8",
  4071 + "react-dom": ">=16.8"
  4072 + }
  4073 + },
  4074 + "node_modules/redent": {
  4075 + "version": "3.0.0",
  4076 + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
  4077 + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
  4078 + "dev": true,
  4079 + "license": "MIT",
  4080 + "dependencies": {
  4081 + "indent-string": "^4.0.0",
  4082 + "strip-indent": "^3.0.0"
  4083 + },
  4084 + "engines": {
  4085 + "node": ">=8"
  4086 + }
  4087 + },
  4088 + "node_modules/redux": {
  4089 + "version": "5.0.1",
  4090 + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
  4091 + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
  4092 + "license": "MIT"
  4093 + },
  4094 + "node_modules/redux-thunk": {
  4095 + "version": "3.1.0",
  4096 + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
  4097 + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
  4098 + "license": "MIT",
  4099 + "peerDependencies": {
  4100 + "redux": "^5.0.0"
  4101 + }
  4102 + },
  4103 + "node_modules/requires-port": {
  4104 + "version": "1.0.0",
  4105 + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
  4106 + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
  4107 + "dev": true,
  4108 + "license": "MIT"
  4109 + },
  4110 + "node_modules/reselect": {
  4111 + "version": "5.1.1",
  4112 + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
  4113 + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
  4114 + "license": "MIT"
  4115 + },
  4116 + "node_modules/resize-observer-polyfill": {
  4117 + "version": "1.5.1",
  4118 + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
  4119 + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
  4120 + "license": "MIT"
  4121 + },
  4122 + "node_modules/rollup": {
  4123 + "version": "4.60.3",
  4124 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz",
  4125 + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==",
  4126 + "dev": true,
  4127 + "license": "MIT",
  4128 + "dependencies": {
  4129 + "@types/estree": "1.0.8"
  4130 + },
  4131 + "bin": {
  4132 + "rollup": "dist/bin/rollup"
  4133 + },
  4134 + "engines": {
  4135 + "node": ">=18.0.0",
  4136 + "npm": ">=8.0.0"
  4137 + },
  4138 + "optionalDependencies": {
  4139 + "@rollup/rollup-android-arm-eabi": "4.60.3",
  4140 + "@rollup/rollup-android-arm64": "4.60.3",
  4141 + "@rollup/rollup-darwin-arm64": "4.60.3",
  4142 + "@rollup/rollup-darwin-x64": "4.60.3",
  4143 + "@rollup/rollup-freebsd-arm64": "4.60.3",
  4144 + "@rollup/rollup-freebsd-x64": "4.60.3",
  4145 + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3",
  4146 + "@rollup/rollup-linux-arm-musleabihf": "4.60.3",
  4147 + "@rollup/rollup-linux-arm64-gnu": "4.60.3",
  4148 + "@rollup/rollup-linux-arm64-musl": "4.60.3",
  4149 + "@rollup/rollup-linux-loong64-gnu": "4.60.3",
  4150 + "@rollup/rollup-linux-loong64-musl": "4.60.3",
  4151 + "@rollup/rollup-linux-ppc64-gnu": "4.60.3",
  4152 + "@rollup/rollup-linux-ppc64-musl": "4.60.3",
  4153 + "@rollup/rollup-linux-riscv64-gnu": "4.60.3",
  4154 + "@rollup/rollup-linux-riscv64-musl": "4.60.3",
  4155 + "@rollup/rollup-linux-s390x-gnu": "4.60.3",
  4156 + "@rollup/rollup-linux-x64-gnu": "4.60.3",
  4157 + "@rollup/rollup-linux-x64-musl": "4.60.3",
  4158 + "@rollup/rollup-openbsd-x64": "4.60.3",
  4159 + "@rollup/rollup-openharmony-arm64": "4.60.3",
  4160 + "@rollup/rollup-win32-arm64-msvc": "4.60.3",
  4161 + "@rollup/rollup-win32-ia32-msvc": "4.60.3",
  4162 + "@rollup/rollup-win32-x64-gnu": "4.60.3",
  4163 + "@rollup/rollup-win32-x64-msvc": "4.60.3",
  4164 + "fsevents": "~2.3.2"
  4165 + }
  4166 + },
  4167 + "node_modules/rrweb-cssom": {
  4168 + "version": "0.7.1",
  4169 + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
  4170 + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
  4171 + "dev": true,
  4172 + "license": "MIT"
  4173 + },
  4174 + "node_modules/safer-buffer": {
  4175 + "version": "2.1.2",
  4176 + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
  4177 + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
  4178 + "dev": true,
  4179 + "license": "MIT"
  4180 + },
  4181 + "node_modules/saxes": {
  4182 + "version": "6.0.0",
  4183 + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
  4184 + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
  4185 + "dev": true,
  4186 + "license": "ISC",
  4187 + "dependencies": {
  4188 + "xmlchars": "^2.2.0"
  4189 + },
  4190 + "engines": {
  4191 + "node": ">=v12.22.7"
  4192 + }
  4193 + },
  4194 + "node_modules/scheduler": {
  4195 + "version": "0.23.2",
  4196 + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
  4197 + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
  4198 + "license": "MIT",
  4199 + "dependencies": {
  4200 + "loose-envify": "^1.1.0"
  4201 + }
  4202 + },
  4203 + "node_modules/scroll-into-view-if-needed": {
  4204 + "version": "3.1.0",
  4205 + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
  4206 + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
  4207 + "license": "MIT",
  4208 + "dependencies": {
  4209 + "compute-scroll-into-view": "^3.0.2"
  4210 + }
  4211 + },
  4212 + "node_modules/semver": {
  4213 + "version": "6.3.1",
  4214 + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
  4215 + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
  4216 + "dev": true,
  4217 + "license": "ISC",
  4218 + "bin": {
  4219 + "semver": "bin/semver.js"
  4220 + }
  4221 + },
  4222 + "node_modules/shebang-command": {
  4223 + "version": "2.0.0",
  4224 + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
  4225 + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
  4226 + "dev": true,
  4227 + "license": "MIT",
  4228 + "dependencies": {
  4229 + "shebang-regex": "^3.0.0"
  4230 + },
  4231 + "engines": {
  4232 + "node": ">=8"
  4233 + }
  4234 + },
  4235 + "node_modules/shebang-regex": {
  4236 + "version": "3.0.0",
  4237 + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
  4238 + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
  4239 + "dev": true,
  4240 + "license": "MIT",
  4241 + "engines": {
  4242 + "node": ">=8"
  4243 + }
  4244 + },
  4245 + "node_modules/siginfo": {
  4246 + "version": "2.0.0",
  4247 + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
  4248 + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
  4249 + "dev": true,
  4250 + "license": "ISC"
  4251 + },
  4252 + "node_modules/signal-exit": {
  4253 + "version": "4.1.0",
  4254 + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
  4255 + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
  4256 + "dev": true,
  4257 + "license": "ISC",
  4258 + "engines": {
  4259 + "node": ">=14"
  4260 + },
  4261 + "funding": {
  4262 + "url": "https://github.com/sponsors/isaacs"
  4263 + }
  4264 + },
  4265 + "node_modules/source-map-js": {
  4266 + "version": "1.2.1",
  4267 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
  4268 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
  4269 + "dev": true,
  4270 + "license": "BSD-3-Clause",
  4271 + "engines": {
  4272 + "node": ">=0.10.0"
  4273 + }
  4274 + },
  4275 + "node_modules/stackback": {
  4276 + "version": "0.0.2",
  4277 + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
  4278 + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
  4279 + "dev": true,
  4280 + "license": "MIT"
  4281 + },
  4282 + "node_modules/std-env": {
  4283 + "version": "3.10.0",
  4284 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
  4285 + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
  4286 + "dev": true,
  4287 + "license": "MIT"
  4288 + },
  4289 + "node_modules/string-convert": {
  4290 + "version": "0.2.1",
  4291 + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
  4292 + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
  4293 + "license": "MIT"
  4294 + },
  4295 + "node_modules/strip-final-newline": {
  4296 + "version": "3.0.0",
  4297 + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
  4298 + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
  4299 + "dev": true,
  4300 + "license": "MIT",
  4301 + "engines": {
  4302 + "node": ">=12"
  4303 + },
  4304 + "funding": {
  4305 + "url": "https://github.com/sponsors/sindresorhus"
  4306 + }
  4307 + },
  4308 + "node_modules/strip-indent": {
  4309 + "version": "3.0.0",
  4310 + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
  4311 + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
  4312 + "dev": true,
  4313 + "license": "MIT",
  4314 + "dependencies": {
  4315 + "min-indent": "^1.0.0"
  4316 + },
  4317 + "engines": {
  4318 + "node": ">=8"
  4319 + }
  4320 + },
  4321 + "node_modules/strip-literal": {
  4322 + "version": "2.1.1",
  4323 + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz",
  4324 + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==",
  4325 + "dev": true,
  4326 + "license": "MIT",
  4327 + "dependencies": {
  4328 + "js-tokens": "^9.0.1"
  4329 + },
  4330 + "funding": {
  4331 + "url": "https://github.com/sponsors/antfu"
  4332 + }
  4333 + },
  4334 + "node_modules/strip-literal/node_modules/js-tokens": {
  4335 + "version": "9.0.1",
  4336 + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
  4337 + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
  4338 + "dev": true,
  4339 + "license": "MIT"
  4340 + },
  4341 + "node_modules/stylis": {
  4342 + "version": "4.4.0",
  4343 + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
  4344 + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
  4345 + "license": "MIT"
  4346 + },
  4347 + "node_modules/symbol-tree": {
  4348 + "version": "3.2.4",
  4349 + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
  4350 + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
  4351 + "dev": true,
  4352 + "license": "MIT"
  4353 + },
  4354 + "node_modules/throttle-debounce": {
  4355 + "version": "5.0.2",
  4356 + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
  4357 + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
  4358 + "license": "MIT",
  4359 + "engines": {
  4360 + "node": ">=12.22"
  4361 + }
  4362 + },
  4363 + "node_modules/tinybench": {
  4364 + "version": "2.9.0",
  4365 + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
  4366 + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
  4367 + "dev": true,
  4368 + "license": "MIT"
  4369 + },
  4370 + "node_modules/tinypool": {
  4371 + "version": "0.8.4",
  4372 + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz",
  4373 + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==",
  4374 + "dev": true,
  4375 + "license": "MIT",
  4376 + "engines": {
  4377 + "node": ">=14.0.0"
  4378 + }
  4379 + },
  4380 + "node_modules/tinyspy": {
  4381 + "version": "2.2.1",
  4382 + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
  4383 + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
  4384 + "dev": true,
  4385 + "license": "MIT",
  4386 + "engines": {
  4387 + "node": ">=14.0.0"
  4388 + }
  4389 + },
  4390 + "node_modules/toggle-selection": {
  4391 + "version": "1.0.6",
  4392 + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
  4393 + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
  4394 + "license": "MIT"
  4395 + },
  4396 + "node_modules/tough-cookie": {
  4397 + "version": "4.1.4",
  4398 + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
  4399 + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
  4400 + "dev": true,
  4401 + "license": "BSD-3-Clause",
  4402 + "dependencies": {
  4403 + "psl": "^1.1.33",
  4404 + "punycode": "^2.1.1",
  4405 + "universalify": "^0.2.0",
  4406 + "url-parse": "^1.5.3"
  4407 + },
  4408 + "engines": {
  4409 + "node": ">=6"
  4410 + }
  4411 + },
  4412 + "node_modules/tr46": {
  4413 + "version": "5.1.1",
  4414 + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
  4415 + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
  4416 + "dev": true,
  4417 + "license": "MIT",
  4418 + "dependencies": {
  4419 + "punycode": "^2.3.1"
  4420 + },
  4421 + "engines": {
  4422 + "node": ">=18"
  4423 + }
  4424 + },
  4425 + "node_modules/type-detect": {
  4426 + "version": "4.1.0",
  4427 + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
  4428 + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
  4429 + "dev": true,
  4430 + "license": "MIT",
  4431 + "engines": {
  4432 + "node": ">=4"
  4433 + }
  4434 + },
  4435 + "node_modules/typescript": {
  4436 + "version": "5.9.3",
  4437 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
  4438 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
  4439 + "dev": true,
  4440 + "license": "Apache-2.0",
  4441 + "bin": {
  4442 + "tsc": "bin/tsc",
  4443 + "tsserver": "bin/tsserver"
  4444 + },
  4445 + "engines": {
  4446 + "node": ">=14.17"
  4447 + }
  4448 + },
  4449 + "node_modules/ufo": {
  4450 + "version": "1.6.4",
  4451 + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
  4452 + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
  4453 + "dev": true,
  4454 + "license": "MIT"
  4455 + },
  4456 + "node_modules/universalify": {
  4457 + "version": "0.2.0",
  4458 + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
  4459 + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
  4460 + "dev": true,
  4461 + "license": "MIT",
  4462 + "engines": {
  4463 + "node": ">= 4.0.0"
  4464 + }
  4465 + },
  4466 + "node_modules/update-browserslist-db": {
  4467 + "version": "1.2.3",
  4468 + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
  4469 + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
  4470 + "dev": true,
  4471 + "funding": [
  4472 + {
  4473 + "type": "opencollective",
  4474 + "url": "https://opencollective.com/browserslist"
  4475 + },
  4476 + {
  4477 + "type": "tidelift",
  4478 + "url": "https://tidelift.com/funding/github/npm/browserslist"
  4479 + },
  4480 + {
  4481 + "type": "github",
  4482 + "url": "https://github.com/sponsors/ai"
  4483 + }
  4484 + ],
  4485 + "license": "MIT",
  4486 + "dependencies": {
  4487 + "escalade": "^3.2.0",
  4488 + "picocolors": "^1.1.1"
  4489 + },
  4490 + "bin": {
  4491 + "update-browserslist-db": "cli.js"
  4492 + },
  4493 + "peerDependencies": {
  4494 + "browserslist": ">= 4.21.0"
  4495 + }
  4496 + },
  4497 + "node_modules/url-parse": {
  4498 + "version": "1.5.10",
  4499 + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
  4500 + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
  4501 + "dev": true,
  4502 + "license": "MIT",
  4503 + "dependencies": {
  4504 + "querystringify": "^2.1.1",
  4505 + "requires-port": "^1.0.0"
  4506 + }
  4507 + },
  4508 + "node_modules/use-sync-external-store": {
  4509 + "version": "1.6.0",
  4510 + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
  4511 + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
  4512 + "license": "MIT",
  4513 + "peerDependencies": {
  4514 + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
  4515 + }
  4516 + },
  4517 + "node_modules/vite": {
  4518 + "version": "5.4.21",
  4519 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
  4520 + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
  4521 + "dev": true,
  4522 + "license": "MIT",
  4523 + "dependencies": {
  4524 + "esbuild": "^0.21.3",
  4525 + "postcss": "^8.4.43",
  4526 + "rollup": "^4.20.0"
  4527 + },
  4528 + "bin": {
  4529 + "vite": "bin/vite.js"
  4530 + },
  4531 + "engines": {
  4532 + "node": "^18.0.0 || >=20.0.0"
  4533 + },
  4534 + "funding": {
  4535 + "url": "https://github.com/vitejs/vite?sponsor=1"
  4536 + },
  4537 + "optionalDependencies": {
  4538 + "fsevents": "~2.3.3"
  4539 + },
  4540 + "peerDependencies": {
  4541 + "@types/node": "^18.0.0 || >=20.0.0",
  4542 + "less": "*",
  4543 + "lightningcss": "^1.21.0",
  4544 + "sass": "*",
  4545 + "sass-embedded": "*",
  4546 + "stylus": "*",
  4547 + "sugarss": "*",
  4548 + "terser": "^5.4.0"
  4549 + },
  4550 + "peerDependenciesMeta": {
  4551 + "@types/node": {
  4552 + "optional": true
  4553 + },
  4554 + "less": {
  4555 + "optional": true
  4556 + },
  4557 + "lightningcss": {
  4558 + "optional": true
  4559 + },
  4560 + "sass": {
  4561 + "optional": true
  4562 + },
  4563 + "sass-embedded": {
  4564 + "optional": true
  4565 + },
  4566 + "stylus": {
  4567 + "optional": true
  4568 + },
  4569 + "sugarss": {
  4570 + "optional": true
  4571 + },
  4572 + "terser": {
  4573 + "optional": true
  4574 + }
  4575 + }
  4576 + },
  4577 + "node_modules/vite-node": {
  4578 + "version": "1.6.1",
  4579 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz",
  4580 + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==",
  4581 + "dev": true,
  4582 + "license": "MIT",
  4583 + "dependencies": {
  4584 + "cac": "^6.7.14",
  4585 + "debug": "^4.3.4",
  4586 + "pathe": "^1.1.1",
  4587 + "picocolors": "^1.0.0",
  4588 + "vite": "^5.0.0"
  4589 + },
  4590 + "bin": {
  4591 + "vite-node": "vite-node.mjs"
  4592 + },
  4593 + "engines": {
  4594 + "node": "^18.0.0 || >=20.0.0"
  4595 + },
  4596 + "funding": {
  4597 + "url": "https://opencollective.com/vitest"
  4598 + }
  4599 + },
  4600 + "node_modules/vitest": {
  4601 + "version": "1.6.1",
  4602 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz",
  4603 + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==",
  4604 + "dev": true,
  4605 + "license": "MIT",
  4606 + "dependencies": {
  4607 + "@vitest/expect": "1.6.1",
  4608 + "@vitest/runner": "1.6.1",
  4609 + "@vitest/snapshot": "1.6.1",
  4610 + "@vitest/spy": "1.6.1",
  4611 + "@vitest/utils": "1.6.1",
  4612 + "acorn-walk": "^8.3.2",
  4613 + "chai": "^4.3.10",
  4614 + "debug": "^4.3.4",
  4615 + "execa": "^8.0.1",
  4616 + "local-pkg": "^0.5.0",
  4617 + "magic-string": "^0.30.5",
  4618 + "pathe": "^1.1.1",
  4619 + "picocolors": "^1.0.0",
  4620 + "std-env": "^3.5.0",
  4621 + "strip-literal": "^2.0.0",
  4622 + "tinybench": "^2.5.1",
  4623 + "tinypool": "^0.8.3",
  4624 + "vite": "^5.0.0",
  4625 + "vite-node": "1.6.1",
  4626 + "why-is-node-running": "^2.2.2"
  4627 + },
  4628 + "bin": {
  4629 + "vitest": "vitest.mjs"
  4630 + },
  4631 + "engines": {
  4632 + "node": "^18.0.0 || >=20.0.0"
  4633 + },
  4634 + "funding": {
  4635 + "url": "https://opencollective.com/vitest"
  4636 + },
  4637 + "peerDependencies": {
  4638 + "@edge-runtime/vm": "*",
  4639 + "@types/node": "^18.0.0 || >=20.0.0",
  4640 + "@vitest/browser": "1.6.1",
  4641 + "@vitest/ui": "1.6.1",
  4642 + "happy-dom": "*",
  4643 + "jsdom": "*"
  4644 + },
  4645 + "peerDependenciesMeta": {
  4646 + "@edge-runtime/vm": {
  4647 + "optional": true
  4648 + },
  4649 + "@types/node": {
  4650 + "optional": true
  4651 + },
  4652 + "@vitest/browser": {
  4653 + "optional": true
  4654 + },
  4655 + "@vitest/ui": {
  4656 + "optional": true
  4657 + },
  4658 + "happy-dom": {
  4659 + "optional": true
  4660 + },
  4661 + "jsdom": {
  4662 + "optional": true
  4663 + }
  4664 + }
  4665 + },
  4666 + "node_modules/w3c-xmlserializer": {
  4667 + "version": "5.0.0",
  4668 + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
  4669 + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
  4670 + "dev": true,
  4671 + "license": "MIT",
  4672 + "dependencies": {
  4673 + "xml-name-validator": "^5.0.0"
  4674 + },
  4675 + "engines": {
  4676 + "node": ">=18"
  4677 + }
  4678 + },
  4679 + "node_modules/webidl-conversions": {
  4680 + "version": "7.0.0",
  4681 + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
  4682 + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
  4683 + "dev": true,
  4684 + "license": "BSD-2-Clause",
  4685 + "engines": {
  4686 + "node": ">=12"
  4687 + }
  4688 + },
  4689 + "node_modules/whatwg-encoding": {
  4690 + "version": "3.1.1",
  4691 + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
  4692 + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
  4693 + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
  4694 + "dev": true,
  4695 + "license": "MIT",
  4696 + "dependencies": {
  4697 + "iconv-lite": "0.6.3"
  4698 + },
  4699 + "engines": {
  4700 + "node": ">=18"
  4701 + }
  4702 + },
  4703 + "node_modules/whatwg-mimetype": {
  4704 + "version": "4.0.0",
  4705 + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
  4706 + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
  4707 + "dev": true,
  4708 + "license": "MIT",
  4709 + "engines": {
  4710 + "node": ">=18"
  4711 + }
  4712 + },
  4713 + "node_modules/whatwg-url": {
  4714 + "version": "14.2.0",
  4715 + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
  4716 + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
  4717 + "dev": true,
  4718 + "license": "MIT",
  4719 + "dependencies": {
  4720 + "tr46": "^5.1.0",
  4721 + "webidl-conversions": "^7.0.0"
  4722 + },
  4723 + "engines": {
  4724 + "node": ">=18"
  4725 + }
  4726 + },
  4727 + "node_modules/which": {
  4728 + "version": "2.0.2",
  4729 + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
  4730 + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
  4731 + "dev": true,
  4732 + "license": "ISC",
  4733 + "dependencies": {
  4734 + "isexe": "^2.0.0"
  4735 + },
  4736 + "bin": {
  4737 + "node-which": "bin/node-which"
  4738 + },
  4739 + "engines": {
  4740 + "node": ">= 8"
  4741 + }
  4742 + },
  4743 + "node_modules/why-is-node-running": {
  4744 + "version": "2.3.0",
  4745 + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
  4746 + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
  4747 + "dev": true,
  4748 + "license": "MIT",
  4749 + "dependencies": {
  4750 + "siginfo": "^2.0.0",
  4751 + "stackback": "0.0.2"
  4752 + },
  4753 + "bin": {
  4754 + "why-is-node-running": "cli.js"
  4755 + },
  4756 + "engines": {
  4757 + "node": ">=8"
  4758 + }
  4759 + },
  4760 + "node_modules/ws": {
  4761 + "version": "8.20.0",
  4762 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
  4763 + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
  4764 + "dev": true,
  4765 + "license": "MIT",
  4766 + "engines": {
  4767 + "node": ">=10.0.0"
  4768 + },
  4769 + "peerDependencies": {
  4770 + "bufferutil": "^4.0.1",
  4771 + "utf-8-validate": ">=5.0.2"
  4772 + },
  4773 + "peerDependenciesMeta": {
  4774 + "bufferutil": {
  4775 + "optional": true
  4776 + },
  4777 + "utf-8-validate": {
  4778 + "optional": true
  4779 + }
  4780 + }
  4781 + },
  4782 + "node_modules/xml-name-validator": {
  4783 + "version": "5.0.0",
  4784 + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
  4785 + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
  4786 + "dev": true,
  4787 + "license": "Apache-2.0",
  4788 + "engines": {
  4789 + "node": ">=18"
  4790 + }
  4791 + },
  4792 + "node_modules/xmlchars": {
  4793 + "version": "2.2.0",
  4794 + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
  4795 + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
  4796 + "dev": true,
  4797 + "license": "MIT"
  4798 + },
  4799 + "node_modules/yallist": {
  4800 + "version": "3.1.1",
  4801 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
  4802 + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
  4803 + "dev": true,
  4804 + "license": "ISC"
  4805 + },
  4806 + "node_modules/yocto-queue": {
  4807 + "version": "1.2.2",
  4808 + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
  4809 + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
  4810 + "dev": true,
  4811 + "license": "MIT",
  4812 + "engines": {
  4813 + "node": ">=12.20"
  4814 + },
  4815 + "funding": {
  4816 + "url": "https://github.com/sponsors/sindresorhus"
  4817 + }
  4818 + }
  4819 + }
  4820 +}
... ...
frontend/package.json 0 → 100644
  1 +{
  2 + "name": "erp-frontend",
  3 + "private": true,
  4 + "version": "0.0.1",
  5 + "type": "module",
  6 + "scripts": {
  7 + "dev": "vite",
  8 + "build": "tsc && vite build",
  9 + "lint": "tsc --noEmit",
  10 + "preview": "vite preview",
  11 + "test": "vitest"
  12 + },
  13 + "dependencies": {
  14 + "@ant-design/icons": "^5.3.7",
  15 + "@reduxjs/toolkit": "^2.2.5",
  16 + "antd": "^5.18.0",
  17 + "axios": "^1.7.2",
  18 + "dayjs": "^1.11.11",
  19 + "react": "^18.3.1",
  20 + "react-dom": "^18.3.1",
  21 + "react-redux": "^9.1.2",
  22 + "react-router-dom": "^6.23.1"
  23 + },
  24 + "devDependencies": {
  25 + "@testing-library/jest-dom": "^6.4.6",
  26 + "@testing-library/react": "^16.0.0",
  27 + "@testing-library/user-event": "^14.5.2",
  28 + "@types/react": "^18.3.3",
  29 + "@types/react-dom": "^18.3.0",
  30 + "@vitejs/plugin-react": "^4.3.1",
  31 + "jsdom": "^24.1.0",
  32 + "typescript": "^5.4.5",
  33 + "vite": "^5.3.1",
  34 + "vitest": "^1.6.0"
  35 + }
  36 +}
... ...
frontend/src/App.tsx 0 → 100644
  1 +import { Navigate, Route, Routes } from 'react-router-dom'
  2 +import { useAppSelector } from './store/hooks'
  3 +import LoginPage from './pages/usr/LoginPage'
  4 +import UserListPage from './pages/usr/UserListPage'
  5 +
  6 +function PrivateRoute({ children }: { children: React.ReactNode }) {
  7 + const accessToken = useAppSelector(s => s.auth.accessToken)
  8 + return accessToken ? <>{children}</> : <Navigate to="/login" replace />
  9 +}
  10 +
  11 +export default function App() {
  12 + return (
  13 + <Routes>
  14 + <Route path="/login" element={<LoginPage />} />
  15 + <Route path="/" element={<PrivateRoute><div>主页(待实现)</div></PrivateRoute>} />
  16 + <Route path="/usr/users" element={<PrivateRoute><UserListPage /></PrivateRoute>} />
  17 + <Route path="*" element={<Navigate to="/" replace />} />
  18 + </Routes>
  19 + )
  20 +}
... ...
frontend/src/api/auth.ts 0 → 100644
  1 +import { UserInfoVO } from '../store/slices/authSlice'
  2 +import request from './request'
  3 +
  4 +export interface BrandVO {
  5 + sNo: string
  6 + sName: string
  7 +}
  8 +
  9 +export interface LoginVO {
  10 + accessToken: string
  11 + refreshToken: string
  12 + expiresIn: number
  13 + userInfo: UserInfoVO
  14 +}
  15 +
  16 +export function login(params: { brandNo: string; username: string; password: string }): Promise<LoginVO> {
  17 + return request.post('/auth/login', params)
  18 +}
  19 +
  20 +export function refresh(refreshToken: string): Promise<{ accessToken: string }> {
  21 + return request.post('/auth/refresh', { refreshToken })
  22 +}
  23 +
  24 +export function getBrands(): Promise<BrandVO[]> {
  25 + return request.get('/auth/brands')
  26 +}
... ...
frontend/src/api/request.ts 0 → 100644
  1 +import axios, { AxiosInstance, AxiosResponse } from 'axios'
  2 +import { store } from '../store'
  3 +import { clearCredentials, setCredentials } from '../store/slices/authSlice'
  4 +
  5 +let isRefreshing = false
  6 +let pendingQueue: Array<(token: string) => void> = []
  7 +
  8 +const instance: AxiosInstance = axios.create({
  9 + baseURL: '/api',
  10 + timeout: 10000
  11 +})
  12 +
  13 +instance.interceptors.request.use(config => {
  14 + const token = store.getState().auth.accessToken
  15 + if (token) {
  16 + config.headers.Authorization = `Bearer ${token}`
  17 + }
  18 + return config
  19 +})
  20 +
  21 +instance.interceptors.response.use(
  22 + (response: AxiosResponse) => {
  23 + const data = response.data
  24 + if (data.code !== undefined && data.code !== 200) {
  25 + return Promise.reject(new Error(data.message || '请求失败'))
  26 + }
  27 + return data.data
  28 + },
  29 + async error => {
  30 + const originalRequest = error.config
  31 + if (error.response?.status === 401 && !originalRequest._retry) {
  32 + originalRequest._retry = true
  33 + const refreshToken = store.getState().auth.refreshToken
  34 + if (!refreshToken) {
  35 + store.dispatch(clearCredentials())
  36 + window.location.href = '/login'
  37 + return Promise.reject(error)
  38 + }
  39 + if (isRefreshing) {
  40 + return new Promise(resolve => {
  41 + pendingQueue.push((token: string) => {
  42 + originalRequest.headers.Authorization = `Bearer ${token}`
  43 + resolve(instance(originalRequest))
  44 + })
  45 + })
  46 + }
  47 + isRefreshing = true
  48 + try {
  49 + const res = await axios.post('/api/auth/refresh', { refreshToken })
  50 + const newToken = res.data.data.accessToken
  51 + store.dispatch(setCredentials({
  52 + accessToken: newToken,
  53 + refreshToken,
  54 + userInfo: store.getState().auth.userInfo!
  55 + }))
  56 + pendingQueue.forEach(cb => cb(newToken))
  57 + pendingQueue = []
  58 + originalRequest.headers.Authorization = `Bearer ${newToken}`
  59 + return instance(originalRequest)
  60 + } catch {
  61 + store.dispatch(clearCredentials())
  62 + window.location.href = '/login'
  63 + return Promise.reject(error)
  64 + } finally {
  65 + isRefreshing = false
  66 + }
  67 + }
  68 + return Promise.reject(error)
  69 + }
  70 +)
  71 +
  72 +export default instance
... ...
frontend/src/api/usr.ts 0 → 100644
  1 +import request from './request'
  2 +
  3 +export interface StaffVO {
  4 + sId: string
  5 + sStaffName: string
  6 +}
  7 +
  8 +export interface PermissionGroupVO {
  9 + sId: string
  10 + sGroupCode: string
  11 + sGroupName: string
  12 + sCategory: string | null
  13 +}
  14 +
  15 +export interface UserCreateReq {
  16 + userCode: string
  17 + username: string
  18 + userType: '普通用户' | '超级管理员'
  19 + language: '中文' | '英文' | '繁体'
  20 + canEditDoc?: boolean
  21 + employeeId?: string | null
  22 + permGroupIds?: string[]
  23 +}
  24 +
  25 +export interface UserCreateResp {
  26 + userId: string
  27 + userCode: string
  28 + username: string
  29 +}
  30 +
  31 +export function getStaffs(): Promise<StaffVO[]> {
  32 + return request.get('/usr/users/staffs')
  33 +}
  34 +
  35 +export function getPermissionGroups(): Promise<PermissionGroupVO[]> {
  36 + return request.get('/usr/users/permission-groups')
  37 +}
  38 +
  39 +export function createUser(req: UserCreateReq): Promise<UserCreateResp> {
  40 + return request.post('/usr/users', req)
  41 +}
  42 +
  43 +export interface UserListQueryReq {
  44 + queryField?: string
  45 + matchType?: string
  46 + queryValue?: string
  47 + page?: number
  48 + pageSize?: number
  49 +}
  50 +
  51 +export interface UserListItemVO {
  52 + sId: string
  53 + sUsername: string
  54 + sUserCode: string
  55 + sUserType: string
  56 + sLanguage: string
  57 + bCanEditDoc: number
  58 + bIsDisabled: number
  59 + tLastLoginDate: string | null
  60 + sCreatorUsername: string | null
  61 + tCreateDate: string
  62 + sStaffName: string | null
  63 + sDepartment: string | null
  64 +}
  65 +
  66 +export interface PageVO<T> {
  67 + total: number
  68 + page: number
  69 + pageSize: number
  70 + list: T[]
  71 +}
  72 +
  73 +export function getUserList(params?: UserListQueryReq): Promise<PageVO<UserListItemVO>> {
  74 + return request.get('/usr/users', { params })
  75 +}
  76 +
  77 +export interface UserUpdateReq {
  78 + userType: string
  79 + language: string
  80 + canEditDoc: boolean
  81 + isDisabled: boolean
  82 + employeeId: string | null
  83 + permGroupIds: string[]
  84 +}
  85 +
  86 +export interface UserUpdateResp {
  87 + userId: string
  88 + username: string
  89 + updatedAt: string
  90 +}
  91 +
  92 +export function updateUser(userId: string, req: UserUpdateReq): Promise<UserUpdateResp> {
  93 + return request.put(`/usr/users/${userId}`, req)
  94 +}
... ...
frontend/src/components/PermButton.tsx 0 → 100644
  1 +import { Button, ButtonProps } from 'antd'
  2 +import { useAppSelector } from '../store/hooks'
  3 +
  4 +interface PermButtonProps extends ButtonProps {
  5 + permission: string
  6 +}
  7 +
  8 +export function PermButton({ permission: _permission, children, ...props }: PermButtonProps) {
  9 + const userType = useAppSelector(s => s.auth.userInfo?.userType)
  10 + if (userType !== '超级管理员') return null
  11 + return <Button {...props}>{children}</Button>
  12 +}
... ...
frontend/src/main.tsx 0 → 100644
  1 +import React from 'react'
  2 +import ReactDOM from 'react-dom/client'
  3 +import { Provider } from 'react-redux'
  4 +import { BrowserRouter } from 'react-router-dom'
  5 +import App from './App'
  6 +import { store } from './store'
  7 +import './styles/tokens.css'
  8 +
  9 +ReactDOM.createRoot(document.getElementById('root')!).render(
  10 + <React.StrictMode>
  11 + <Provider store={store}>
  12 + <BrowserRouter>
  13 + <App />
  14 + </BrowserRouter>
  15 + </Provider>
  16 + </React.StrictMode>
  17 +)
... ...
frontend/src/pages/usr/LoginPage.tsx 0 → 100644
  1 +import { useEffect, useState } from 'react'
  2 +import { useNavigate } from 'react-router-dom'
  3 +import { Button, Form, Input, message, Select } from 'antd'
  4 +import { getBrands, login, BrandVO } from '../../api/auth'
  5 +import { setCredentials } from '../../store/slices/authSlice'
  6 +import { useAppDispatch } from '../../store/hooks'
  7 +
  8 +export default function LoginPage() {
  9 + const [brandOptions, setBrandOptions] = useState<BrandVO[]>([])
  10 + const [loading, setLoading] = useState(false)
  11 + const [form] = Form.useForm()
  12 + const dispatch = useAppDispatch()
  13 + const navigate = useNavigate()
  14 +
  15 + useEffect(() => {
  16 + getBrands().then(brands => {
  17 + setBrandOptions(brands)
  18 + const std = brands.find(b => b.sName === '标准版')
  19 + const defaultNo = std ? std.sNo : brands[0]?.sNo
  20 + if (defaultNo) form.setFieldValue('brandNo', defaultNo)
  21 + }).catch(() => {})
  22 + }, [])
  23 +
  24 + const onFinish = async (values: { brandNo: string; username: string; password: string }) => {
  25 + setLoading(true)
  26 + try {
  27 + const result = await login(values)
  28 + dispatch(setCredentials({
  29 + accessToken: result.accessToken,
  30 + refreshToken: result.refreshToken,
  31 + userInfo: result.userInfo
  32 + }))
  33 + navigate('/')
  34 + } catch (e: unknown) {
  35 + message.error(e instanceof Error ? e.message : '登录失败')
  36 + } finally {
  37 + setLoading(false)
  38 + }
  39 + }
  40 +
  41 + return (
  42 + <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh' }}>
  43 + <div style={{ width: 360 }}>
  44 + <h2 style={{ textAlign: 'center', marginBottom: 24 }}>小羚羊 ERP</h2>
  45 + <Form form={form} layout="vertical" onFinish={onFinish}>
  46 + <Form.Item label="公司/版本" name="brandNo" rules={[{ required: true, message: '请选择公司/版本' }]}>
  47 + <Select
  48 + options={brandOptions.map(b => ({ value: b.sNo, label: b.sName }))}
  49 + placeholder="请选择公司/版本"
  50 + />
  51 + </Form.Item>
  52 + <Form.Item label="用户名" name="username" rules={[{ required: true, message: '请输入用户名' }]}>
  53 + <Input placeholder="请输入用户名" />
  54 + </Form.Item>
  55 + <Form.Item label="密码" name="password" rules={[{ required: true, message: '请输入密码' }]}>
  56 + <Input.Password placeholder="请输入密码" />
  57 + </Form.Item>
  58 + <Form.Item>
  59 + <Button type="primary" htmlType="submit" loading={loading} block>
  60 + 登录
  61 + </Button>
  62 + </Form.Item>
  63 + </Form>
  64 + </div>
  65 + </div>
  66 + )
  67 +}
... ...
frontend/src/pages/usr/UserFormDrawer.tsx 0 → 100644
  1 +import { useEffect, useState } from 'react'
  2 +import {
  3 + Drawer, Form, Input, Select, Checkbox, Button, message, Table
  4 +} from 'antd'
  5 +import type { ColumnsType } from 'antd/es/table'
  6 +import { getStaffs, getPermissionGroups, createUser, updateUser, StaffVO, PermissionGroupVO, UserCreateReq, UserUpdateReq } from '../../api/usr'
  7 +
  8 +interface InitialData {
  9 + userType: string
  10 + language: string
  11 + canEditDoc: boolean
  12 + isDisabled: boolean
  13 + employeeId?: string | null
  14 + permGroupIds?: string[]
  15 +}
  16 +
  17 +interface Props {
  18 + open: boolean
  19 + onClose: () => void
  20 + onSuccess: () => void
  21 + userId?: string
  22 + initialData?: InitialData
  23 +}
  24 +
  25 +export default function UserFormDrawer({ open, onClose, onSuccess, userId, initialData }: Props) {
  26 + const [form] = Form.useForm()
  27 + const [staffs, setStaffs] = useState<StaffVO[]>([])
  28 + const [permGroups, setPermGroups] = useState<PermissionGroupVO[]>([])
  29 + const [selectedPermIds, setSelectedPermIds] = useState<string[]>([])
  30 + const [submitting, setSubmitting] = useState(false)
  31 +
  32 + const isEditMode = !!userId
  33 +
  34 + useEffect(() => {
  35 + if (open) {
  36 + getStaffs().then(setStaffs).catch(() => {})
  37 + getPermissionGroups().then(setPermGroups).catch(() => {})
  38 + if (isEditMode && initialData) {
  39 + form.setFieldsValue({
  40 + userType: initialData.userType,
  41 + language: initialData.language,
  42 + canEditDoc: initialData.canEditDoc,
  43 + isDisabled: initialData.isDisabled,
  44 + employeeId: initialData.employeeId ?? null,
  45 + })
  46 + setSelectedPermIds(initialData.permGroupIds ?? [])
  47 + } else {
  48 + form.resetFields()
  49 + setSelectedPermIds([])
  50 + }
  51 + }
  52 + }, [open])
  53 +
  54 + const permColumns: ColumnsType<PermissionGroupVO> = [
  55 + {
  56 + title: '',
  57 + key: 'select',
  58 + width: 40,
  59 + render: (_, record) => (
  60 + <Checkbox
  61 + checked={selectedPermIds.includes(record.sId)}
  62 + onChange={e => {
  63 + setSelectedPermIds(prev =>
  64 + e.target.checked ? [...prev, record.sId] : prev.filter(id => id !== record.sId)
  65 + )
  66 + }}
  67 + />
  68 + )
  69 + },
  70 + { title: '权限组', dataIndex: 'sGroupName' },
  71 + { title: '分类', dataIndex: 'sCategory' }
  72 + ]
  73 +
  74 + async function handleSubmit() {
  75 + try {
  76 + const values = await form.validateFields()
  77 + setSubmitting(true)
  78 + if (isEditMode) {
  79 + const req: UserUpdateReq = {
  80 + userType: values.userType,
  81 + language: values.language,
  82 + canEditDoc: values.canEditDoc ?? false,
  83 + isDisabled: values.isDisabled ?? false,
  84 + employeeId: values.employeeId ?? null,
  85 + permGroupIds: selectedPermIds
  86 + }
  87 + await updateUser(userId, req)
  88 + message.success('修改用户成功')
  89 + } else {
  90 + const req: UserCreateReq = {
  91 + userCode: values.userCode,
  92 + username: values.username,
  93 + userType: values.userType,
  94 + language: values.language,
  95 + canEditDoc: values.canEditDoc ?? false,
  96 + employeeId: values.employeeId ?? null,
  97 + permGroupIds: selectedPermIds
  98 + }
  99 + await createUser(req)
  100 + message.success('新增用户成功')
  101 + }
  102 + form.resetFields()
  103 + setSelectedPermIds([])
  104 + onSuccess()
  105 + } catch (e: unknown) {
  106 + if (e instanceof Error) {
  107 + message.error(e.message)
  108 + }
  109 + } finally {
  110 + setSubmitting(false)
  111 + }
  112 + }
  113 +
  114 + return (
  115 + <Drawer
  116 + title={isEditMode ? '修改用户' : '新增用户'}
  117 + open={open}
  118 + onClose={onClose}
  119 + width={520}
  120 + footer={
  121 + <div style={{ textAlign: 'right' }}>
  122 + <Button onClick={onClose} style={{ marginRight: 8 }}>取消</Button>
  123 + <Button type="primary" onClick={handleSubmit} loading={submitting}>确认</Button>
  124 + </div>
  125 + }
  126 + >
  127 + <Form form={form} layout="vertical">
  128 + {!isEditMode && (
  129 + <>
  130 + <Form.Item name="userCode" label="用户号" rules={[{ required: true, message: '请输入用户号' }]}>
  131 + <Input placeholder="请输入用户号" />
  132 + </Form.Item>
  133 + <Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}>
  134 + <Input placeholder="请输入用户名" />
  135 + </Form.Item>
  136 + </>
  137 + )}
  138 + <Form.Item name="userType" label="用户类型" rules={[{ required: true }]} initialValue="普通用户">
  139 + <Select options={[{ value: '普通用户', label: '普通用户' }, { value: '超级管理员', label: '超级管理员' }]} />
  140 + </Form.Item>
  141 + <Form.Item name="language" label="语言" rules={[{ required: true }]} initialValue="中文">
  142 + <Select options={[{ value: '中文', label: '中文' }, { value: '英文', label: 'English' }, { value: '繁体', label: '繁體' }]} />
  143 + </Form.Item>
  144 + <Form.Item name="canEditDoc" valuePropName="checked">
  145 + <Checkbox>可编辑文档</Checkbox>
  146 + </Form.Item>
  147 + {isEditMode && (
  148 + <Form.Item name="isDisabled" valuePropName="checked">
  149 + <Checkbox>作废</Checkbox>
  150 + </Form.Item>
  151 + )}
  152 + <Form.Item name="employeeId" label="关联员工">
  153 + <Select
  154 + allowClear
  155 + placeholder="选择员工(可选)"
  156 + options={staffs.map(s => ({ value: s.sId, label: s.sStaffName }))}
  157 + />
  158 + </Form.Item>
  159 + <Form.Item label="权限组">
  160 + <Table
  161 + size="small"
  162 + columns={permColumns}
  163 + dataSource={permGroups}
  164 + rowKey="sId"
  165 + pagination={false}
  166 + />
  167 + </Form.Item>
  168 + </Form>
  169 + </Drawer>
  170 + )
  171 +}
... ...
frontend/src/pages/usr/UserListPage.tsx 0 → 100644
  1 +import { useState, useEffect } from 'react'
  2 +import { Table, Select, Input, Button, Space } from 'antd'
  3 +import type { ColumnsType } from 'antd/es/table'
  4 +import { PermButton } from '../../components/PermButton'
  5 +import UserFormDrawer from './UserFormDrawer'
  6 +import { getUserList } from '../../api/usr'
  7 +import type { PageVO, UserListItemVO } from '../../api/usr'
  8 +
  9 +const QUERY_FIELDS = [
  10 + { value: 'username', label: '用户名' },
  11 + { value: 'staffName', label: '员工名' },
  12 + { value: 'userCode', label: '用户号' },
  13 + { value: 'department', label: '部门' },
  14 + { value: 'userType', label: '用户类型' },
  15 + { value: 'disabled', label: '作废' },
  16 + { value: 'lastLoginDate', label: '登录日期' },
  17 + { value: 'creator', label: '制单人' },
  18 +]
  19 +
  20 +const MATCH_TYPES = [
  21 + { value: 'contains', label: '包含' },
  22 + { value: 'notContains', label: '不包含' },
  23 + { value: 'equals', label: '等于' },
  24 +]
  25 +
  26 +// REQ-USR-003: 查询用户
  27 +// REQ-USR-002: 修改用户
  28 +export default function UserListPage() {
  29 + const [drawerOpen, setDrawerOpen] = useState(false)
  30 + const [editingUser, setEditingUser] = useState<UserListItemVO | null>(null)
  31 + const [data, setData] = useState<PageVO<UserListItemVO> | null>(null)
  32 + const [queryField, setQueryField] = useState('username')
  33 + const [matchType, setMatchType] = useState('contains')
  34 + const [queryValue, setQueryValue] = useState('')
  35 + const [currentPage, setCurrentPage] = useState(1)
  36 +
  37 + const load = (pg = 1) => {
  38 + setCurrentPage(pg)
  39 + getUserList({ queryField, matchType, queryValue, page: pg, pageSize: 20 }).then(setData)
  40 + }
  41 +
  42 + useEffect(() => {
  43 + load()
  44 + }, [])
  45 +
  46 + const columns: ColumnsType<UserListItemVO> = [
  47 + { title: '用户名', dataIndex: 'sUsername' },
  48 + { title: '员工名', dataIndex: 'sStaffName' },
  49 + { title: '用户号', dataIndex: 'sUserCode' },
  50 + { title: '部门', dataIndex: 'sDepartment' },
  51 + { title: '用户类型', dataIndex: 'sUserType' },
  52 + { title: '语言', dataIndex: 'sLanguage' },
  53 + { title: '作废', dataIndex: 'bIsDisabled', render: (v: number) => v ? '是' : '否' },
  54 + { title: '登录日期', dataIndex: 'tLastLoginDate' },
  55 + { title: '制单人', dataIndex: 'sCreatorUsername' },
  56 + { title: '制单日期', dataIndex: 'tCreateDate' },
  57 + {
  58 + title: '操作',
  59 + key: 'action',
  60 + render: (_, record) => (
  61 + <PermButton
  62 + permission="usr:edit"
  63 + type="link"
  64 + onClick={() => setEditingUser(record)}
  65 + >
  66 + 修改
  67 + </PermButton>
  68 + )
  69 + },
  70 + ]
  71 +
  72 + return (
  73 + <div>
  74 + <Space style={{ marginBottom: 16 }} wrap>
  75 + <Select
  76 + value={queryField}
  77 + onChange={setQueryField}
  78 + options={QUERY_FIELDS}
  79 + style={{ width: 120 }}
  80 + />
  81 + <Select
  82 + value={matchType}
  83 + onChange={setMatchType}
  84 + options={MATCH_TYPES}
  85 + style={{ width: 100 }}
  86 + />
  87 + <Input
  88 + value={queryValue}
  89 + onChange={e => setQueryValue(e.target.value)}
  90 + placeholder="查询值"
  91 + style={{ width: 160 }}
  92 + />
  93 + <Button type="primary" onClick={() => load(1)}>搜索</Button>
  94 + <PermButton
  95 + permission="usr:create"
  96 + type="primary"
  97 + onClick={() => setDrawerOpen(true)}
  98 + >
  99 + 新增
  100 + </PermButton>
  101 + </Space>
  102 + <Table
  103 + dataSource={data?.list ?? []}
  104 + columns={columns}
  105 + rowKey="sId"
  106 + pagination={{
  107 + total: data?.total ?? 0,
  108 + pageSize: 20,
  109 + current: currentPage,
  110 + onChange: load,
  111 + }}
  112 + />
  113 + <UserFormDrawer
  114 + open={drawerOpen}
  115 + onClose={() => setDrawerOpen(false)}
  116 + onSuccess={() => { setDrawerOpen(false); load(1) }}
  117 + />
  118 + <UserFormDrawer
  119 + open={editingUser !== null}
  120 + userId={editingUser?.sId}
  121 + initialData={editingUser ? {
  122 + userType: editingUser.sUserType,
  123 + language: editingUser.sLanguage,
  124 + canEditDoc: editingUser.bCanEditDoc === 1,
  125 + isDisabled: editingUser.bIsDisabled === 1,
  126 + employeeId: null,
  127 + } : undefined}
  128 + onClose={() => setEditingUser(null)}
  129 + onSuccess={() => { setEditingUser(null); load(1) }}
  130 + />
  131 + </div>
  132 + )
  133 +}
... ...
frontend/src/store/hooks.ts 0 → 100644
  1 +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
  2 +import type { RootState, AppDispatch } from './index'
  3 +
  4 +export const useAppDispatch = () => useDispatch<AppDispatch>()
  5 +export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
... ...
frontend/src/store/index.ts 0 → 100644
  1 +import { configureStore } from '@reduxjs/toolkit'
  2 +import authReducer from './slices/authSlice'
  3 +
  4 +export const store = configureStore({
  5 + reducer: {
  6 + auth: authReducer
  7 + }
  8 +})
  9 +
  10 +export type RootState = ReturnType<typeof store.getState>
  11 +export type AppDispatch = typeof store.dispatch
... ...
frontend/src/store/slices/authSlice.ts 0 → 100644
  1 +import { createSlice, PayloadAction } from '@reduxjs/toolkit'
  2 +
  3 +export interface UserInfoVO {
  4 + userId: string
  5 + username: string
  6 + userType: string
  7 + language: string
  8 + brandId: string
  9 +}
  10 +
  11 +interface AuthState {
  12 + accessToken: string | null
  13 + refreshToken: string | null
  14 + userInfo: UserInfoVO | null
  15 +}
  16 +
  17 +const initialState: AuthState = {
  18 + accessToken: null,
  19 + refreshToken: null,
  20 + userInfo: null
  21 +}
  22 +
  23 +const authSlice = createSlice({
  24 + name: 'auth',
  25 + initialState,
  26 + reducers: {
  27 + setCredentials(state, action: PayloadAction<{ accessToken: string; refreshToken: string; userInfo: UserInfoVO }>) {
  28 + state.accessToken = action.payload.accessToken
  29 + state.refreshToken = action.payload.refreshToken
  30 + state.userInfo = action.payload.userInfo
  31 + },
  32 + clearCredentials(state) {
  33 + state.accessToken = null
  34 + state.refreshToken = null
  35 + state.userInfo = null
  36 + }
  37 + }
  38 +})
  39 +
  40 +export const { setCredentials, clearCredentials } = authSlice.actions
  41 +export default authSlice.reducer
... ...
frontend/src/styles/tokens.css 0 → 100644
  1 +/*
  2 + * src/styles/tokens.css — Design Tokens
  3 + * 命名规范见 docs/04-技术规范.md § 2.5
  4 + * 色值锁定见 docs/06-UI交互规范.md § 四
  5 + *
  6 + * 命名格式:--color-<scope>-<role>-<state>
  7 + * <scope> 组件域:form / table-row / table-header / ...
  8 + * <role> 作用:bg(背景)/ fg(前景/字体)/ border
  9 + * <state> 状态:edit / readonly / hover / selected(无状态时省略)
  10 + *
  11 + * 约束:
  12 + * - 组件样式中只用 var(--color-xxx),禁止硬编码 hex / rgba
  13 + * - 修改色值只改本文件,不允许在组件级覆盖
  14 + * - 新增 token 须先登记到 docs/06 § 4.1 / 4.2,再补到此处
  15 + */
  16 +
  17 +:root {
  18 + /* === 1. 全局调色板(与 Ant Design 主题对齐) === */
  19 + --color-primary: #1890ff;
  20 + --color-success: #52c41a;
  21 + --color-warning: #faad14;
  22 + --color-error: #ff4d4f;
  23 + --color-text: rgba(0, 0, 0, 0.85);
  24 + --color-text-secondary: rgba(0, 0, 0, 0.45);
  25 + --color-border: #d9d9d9;
  26 + --color-bg-base: #f0f2f5;
  27 +
  28 + /* === 2. 组件级状态色(与 docs/06 § 4.2 一一对应) === */
  29 +
  30 + /* form:输入框 / 备注框 / 时间框 / 下拉框共用 */
  31 + --color-form-bg-edit: #ffffff;
  32 + --color-form-bg-readonly: #f1f2f8;
  33 + --color-form-bg-hover: #f5f5f5; /* 仅下拉框使用 */
  34 + --color-form-fg: #000000;
  35 +
  36 + /* table */
  37 + --color-table-row-bg-selected: #86d5fb;
  38 + --color-table-row-bg-hover: #fff7e6;
  39 + --color-table-row-bg-readonly: #f1f2f8; /* = rgb(241, 242, 248) */
  40 + --color-table-row-fg: #000000;
  41 + --color-table-header-bg: #f5f5f5;
  42 + --color-table-header-fg: rgba(0, 0, 0, 0.85); /* = #000000D9 */
  43 +}
... ...
frontend/src/test/LoginPage.test.tsx 0 → 100644
  1 +import { describe, it, expect, vi, beforeEach } from 'vitest'
  2 +import { render, screen, waitFor, act } from '@testing-library/react'
  3 +import userEvent from '@testing-library/user-event'
  4 +import { Provider } from 'react-redux'
  5 +import { MemoryRouter } from 'react-router-dom'
  6 +import { configureStore } from '@reduxjs/toolkit'
  7 +import authReducer, { UserInfoVO } from '../store/slices/authSlice'
  8 +import LoginPage from '../pages/usr/LoginPage'
  9 +import * as authApi from '../api/auth'
  10 +
  11 +vi.mock('../api/auth', () => ({
  12 + getBrands: vi.fn(),
  13 + login: vi.fn()
  14 +}))
  15 +
  16 +vi.mock('../api/request', () => ({
  17 + default: { get: vi.fn(), post: vi.fn() }
  18 +}))
  19 +
  20 +function makeStore() {
  21 + return configureStore({ reducer: { auth: authReducer } })
  22 +}
  23 +
  24 +function renderLoginPage(store = makeStore()) {
  25 + return {
  26 + store,
  27 + ...render(
  28 + <Provider store={store}>
  29 + <MemoryRouter>
  30 + <LoginPage />
  31 + </MemoryRouter>
  32 + </Provider>
  33 + )
  34 + }
  35 +}
  36 +
  37 +describe('LoginPage', () => {
  38 + beforeEach(() => {
  39 + vi.mocked(authApi.getBrands).mockResolvedValue([
  40 + { sNo: 'STD', sName: '标准版' }
  41 + ])
  42 + })
  43 +
  44 + it('renders_brandSelect_username_and_password_fields', async () => {
  45 + renderLoginPage()
  46 + await waitFor(() => expect(screen.getByText('公司/版本')).toBeInTheDocument())
  47 + expect(screen.getByText('用户名')).toBeInTheDocument()
  48 + expect(screen.getByText('密码')).toBeInTheDocument()
  49 + expect(screen.getByRole('button', { name: /登\s*录/ })).toBeInTheDocument()
  50 + })
  51 +
  52 + it('submit_withValidCredentials_dispatchesSetCredentials', async () => {
  53 + const mockUserInfo: UserInfoVO = {
  54 + userId: 'u1', username: 'admin', userType: '超级管理员', language: '中文', brandId: 'b1'
  55 + }
  56 + vi.mocked(authApi.login).mockResolvedValue({
  57 + accessToken: 'at',
  58 + refreshToken: 'rt',
  59 + expiresIn: 86400,
  60 + userInfo: mockUserInfo
  61 + })
  62 +
  63 + const { store } = renderLoginPage()
  64 + // Flush getBrands().then() callback so form.setFieldValue('brandNo') runs before submit
  65 + await act(async () => { await Promise.resolve() })
  66 +
  67 + await userEvent.type(screen.getByPlaceholderText('请输入用户名'), 'admin')
  68 + await userEvent.type(screen.getByPlaceholderText('请输入密码'), '666666')
  69 + await userEvent.click(screen.getByRole('button', { name: /登\s*录/ }))
  70 +
  71 + await waitFor(() => expect(store.getState().auth.accessToken).toBe('at'))
  72 + expect(store.getState().auth.userInfo?.userId).toBe('u1')
  73 + })
  74 +})
... ...
frontend/src/test/UserListPage.test.tsx 0 → 100644
  1 +import { describe, it, expect, vi, beforeEach } from 'vitest'
  2 +import { render, screen, waitFor } from '@testing-library/react'
  3 +import userEvent from '@testing-library/user-event'
  4 +import { Provider } from 'react-redux'
  5 +import { MemoryRouter } from 'react-router-dom'
  6 +import { configureStore } from '@reduxjs/toolkit'
  7 +import authReducer from '../store/slices/authSlice'
  8 +import UserListPage from '../pages/usr/UserListPage'
  9 +
  10 +vi.mock('../api/usr', () => ({
  11 + getStaffs: vi.fn().mockResolvedValue([]),
  12 + getPermissionGroups: vi.fn().mockResolvedValue([]),
  13 + createUser: vi.fn(),
  14 + getUserList: vi.fn().mockResolvedValue({ total: 0, page: 1, pageSize: 20, list: [] }),
  15 + updateUser: vi.fn().mockResolvedValue({ userId: 'u1', username: 'alice', updatedAt: '2026-05-08T10:00:00' })
  16 +}))
  17 +
  18 +vi.mock('../api/request', () => ({
  19 + default: { get: vi.fn(), post: vi.fn() }
  20 +}))
  21 +
  22 +function makeStore(userType: string) {
  23 + const store = configureStore({ reducer: { auth: authReducer } })
  24 + store.dispatch({
  25 + type: 'auth/setCredentials',
  26 + payload: {
  27 + accessToken: 'test-token',
  28 + refreshToken: 'test-refresh',
  29 + userInfo: { userId: 'u1', username: 'admin', userType, language: '中文', brandId: 'b1' }
  30 + }
  31 + })
  32 + return store
  33 +}
  34 +
  35 +function renderPage(userType: string) {
  36 + const store = makeStore(userType)
  37 + return render(
  38 + <Provider store={store}>
  39 + <MemoryRouter>
  40 + <UserListPage />
  41 + </MemoryRouter>
  42 + </Provider>
  43 + )
  44 +}
  45 +
  46 +describe('UserListPage', () => {
  47 + beforeEach(() => {
  48 + vi.clearAllMocks()
  49 + })
  50 +
  51 + it('superAdmin_seesNewButton', () => {
  52 + renderPage('超级管理员')
  53 + expect(screen.getByRole('button', { name: /新\s*增/ })).toBeInTheDocument()
  54 + })
  55 +
  56 + it('normalUser_doesNotSeeNewButton', () => {
  57 + renderPage('普通用户')
  58 + expect(screen.queryByRole('button', { name: /新\s*增/ })).not.toBeInTheDocument()
  59 + })
  60 +
  61 + it('clickNewButton_opensDrawer', async () => {
  62 + renderPage('超级管理员')
  63 + await userEvent.click(screen.getByRole('button', { name: /新\s*增/ }))
  64 + await waitFor(() => expect(screen.getByText('新增用户')).toBeInTheDocument())
  65 + })
  66 +
  67 + it('initialLoad_rendersTableRows', async () => {
  68 + const { getUserList } = await import('../api/usr')
  69 + vi.mocked(getUserList).mockResolvedValueOnce({
  70 + total: 1, page: 1, pageSize: 20,
  71 + list: [{
  72 + sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户',
  73 + sLanguage: '中文', bCanEditDoc: 0, bIsDisabled: 0, tLastLoginDate: null,
  74 + sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00',
  75 + sStaffName: '张三', sDepartment: '研发部'
  76 + }]
  77 + })
  78 + renderPage('超级管理员')
  79 + await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument())
  80 + expect(screen.getByText('张三')).toBeInTheDocument()
  81 + })
  82 +
  83 + it('searchButton_callsGetUserList', async () => {
  84 + const { getUserList } = await import('../api/usr')
  85 + renderPage('超级管理员')
  86 + await userEvent.click(screen.getByRole('button', { name: /搜\s*索|搜索/ }))
  87 + await waitFor(() => expect(vi.mocked(getUserList)).toHaveBeenCalledTimes(2))
  88 + })
  89 +
  90 + it('editMode_submit_callsUpdateUser', async () => {
  91 + const { getUserList, updateUser } = await import('../api/usr')
  92 + vi.mocked(getUserList).mockResolvedValue({
  93 + total: 1, page: 1, pageSize: 20,
  94 + list: [{
  95 + sId: 'u1', sUsername: 'alice', sUserCode: 'UC001', sUserType: '普通用户',
  96 + sLanguage: '中文', bCanEditDoc: 0, bIsDisabled: 0, tLastLoginDate: null,
  97 + sCreatorUsername: 'admin', tCreateDate: '2026-01-01T00:00:00',
  98 + sStaffName: null, sDepartment: null
  99 + }]
  100 + })
  101 + renderPage('超级管理员')
  102 + await waitFor(() => expect(screen.getByText('alice')).toBeInTheDocument())
  103 + await userEvent.click(screen.getByRole('button', { name: /修改/ }))
  104 + await waitFor(() => expect(screen.getByText('修改用户')).toBeInTheDocument())
  105 + await userEvent.click(screen.getByRole('button', { name: /确\s*认/ }))
  106 + await waitFor(() => expect(vi.mocked(updateUser)).toHaveBeenCalledTimes(1))
  107 + })
  108 +})
... ...
frontend/src/test/authSlice.test.ts 0 → 100644
  1 +import { describe, it, expect } from 'vitest'
  2 +import { configureStore } from '@reduxjs/toolkit'
  3 +import authReducer, { setCredentials, clearCredentials, UserInfoVO } from '../store/slices/authSlice'
  4 +
  5 +function makeStore() {
  6 + return configureStore({ reducer: { auth: authReducer } })
  7 +}
  8 +
  9 +const sampleUserInfo: UserInfoVO = {
  10 + userId: 'u1',
  11 + username: 'admin',
  12 + userType: '普通用户',
  13 + language: '中文',
  14 + brandId: 'b1'
  15 +}
  16 +
  17 +describe('authSlice', () => {
  18 + it('setCredentials_updatesAllStateFields', () => {
  19 + const store = makeStore()
  20 + store.dispatch(setCredentials({ accessToken: 't1', refreshToken: 'r1', userInfo: sampleUserInfo }))
  21 + const state = store.getState().auth
  22 + expect(state.accessToken).toBe('t1')
  23 + expect(state.refreshToken).toBe('r1')
  24 + expect(state.userInfo?.userId).toBe('u1')
  25 + })
  26 +
  27 + it('clearCredentials_resetsToNull', () => {
  28 + const store = makeStore()
  29 + store.dispatch(setCredentials({ accessToken: 't1', refreshToken: 'r1', userInfo: sampleUserInfo }))
  30 + store.dispatch(clearCredentials())
  31 + const state = store.getState().auth
  32 + expect(state.accessToken).toBeNull()
  33 + expect(state.refreshToken).toBeNull()
  34 + expect(state.userInfo).toBeNull()
  35 + })
  36 +})
... ...
frontend/src/test/setup.ts 0 → 100644
  1 +import '@testing-library/jest-dom'
  2 +import { vi } from 'vitest'
  3 +
  4 +Object.defineProperty(window, 'matchMedia', {
  5 + writable: true,
  6 + value: vi.fn().mockImplementation(query => ({
  7 + matches: false,
  8 + media: query,
  9 + onchange: null,
  10 + addListener: vi.fn(),
  11 + removeListener: vi.fn(),
  12 + addEventListener: vi.fn(),
  13 + removeEventListener: vi.fn(),
  14 + dispatchEvent: vi.fn()
  15 + }))
  16 +})
... ...
frontend/tsconfig.json 0 → 100644
  1 +{
  2 + "compilerOptions": {
  3 + "target": "ES2020",
  4 + "useDefineForClassFields": true,
  5 + "lib": ["ES2020", "DOM", "DOM.Iterable"],
  6 + "module": "ESNext",
  7 + "skipLibCheck": true,
  8 + "moduleResolution": "bundler",
  9 + "allowImportingTsExtensions": true,
  10 + "resolveJsonModule": true,
  11 + "isolatedModules": true,
  12 + "noEmit": true,
  13 + "jsx": "react-jsx",
  14 + "strict": true,
  15 + "noUnusedLocals": false,
  16 + "noUnusedParameters": false,
  17 + "noFallthroughCasesInSwitch": true
  18 + },
  19 + "include": ["src"],
  20 + "references": [{ "path": "./tsconfig.node.json" }]
  21 +}
... ...
frontend/tsconfig.node.json 0 → 100644
  1 +{
  2 + "compilerOptions": {
  3 + "composite": true,
  4 + "skipLibCheck": true,
  5 + "module": "ESNext",
  6 + "moduleResolution": "bundler",
  7 + "allowSyntheticDefaultImports": true
  8 + },
  9 + "include": ["vite.config.ts"]
  10 +}
... ...
frontend/vite.config.ts 0 → 100644
  1 +import { defineConfig } from 'vite'
  2 +import react from '@vitejs/plugin-react'
  3 +
  4 +export default defineConfig({
  5 + plugins: [react()],
  6 + server: {
  7 + proxy: {
  8 + '/api': {
  9 + target: 'http://localhost:8080',
  10 + changeOrigin: true
  11 + }
  12 + }
  13 + },
  14 + test: {
  15 + environment: 'jsdom',
  16 + globals: true,
  17 + setupFiles: './src/test/setup.ts'
  18 + }
  19 +})
... ...
scripts/test.sh
... ... @@ -8,6 +8,14 @@ set -euo pipefail
8 8 PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
9 9 cd "$PROJECT_ROOT"
10 10  
  11 +# Use Java 21 for Lombok compatibility (system default may be Java 25+)
  12 +if [ -d "/opt/homebrew/Cellar/openjdk@21" ]; then
  13 + JAVA21="$(ls -d /opt/homebrew/Cellar/openjdk@21/*/bin/java 2>/dev/null | tail -1)"
  14 + if [ -n "$JAVA21" ]; then
  15 + export JAVA_HOME="$(dirname "$(dirname "$JAVA21")")"
  16 + fi
  17 +fi
  18 +
11 19 # Stack detection (runtime, mode-agnostic)
12 20 HAS_BACKEND=0; [ -d backend ] && HAS_BACKEND=1
13 21 HAS_FRONTEND=0; [ -d frontend ] && HAS_FRONTEND=1
... ...
sql/migrations/V2__fix_username_unique_per_tenant.sql 0 → 100644
  1 +-- Flyway migration V2 — fix usr_user username uniqueness to per-tenant scope
  2 +-- Generated: 2026-05-08
  3 +-- Reason: uk_usr_user_username was globally unique; same username must be allowed across different brands (sBrandsId)
  4 +-- New unique constraint: (sUsername, sBrandsId) composite
  5 +
  6 +ALTER TABLE usr_user DROP INDEX uk_usr_user_username;
  7 +CREATE UNIQUE INDEX uk_usr_user_username_tenant ON usr_user (sUsername, sBrandsId);
... ...