diff --git a/docker/apitest-mysql.yml b/docker/apitest-mysql.yml
new file mode 100644
index 0000000..051a4e0
--- /dev/null
+++ b/docker/apitest-mysql.yml
@@ -0,0 +1,27 @@
+version: "3.9"
+
+services:
+ mysql-apitest:
+ image: mysql:8.0.32
+ container_name: peipei-mysql-apitest
+ restart: unless-stopped
+ environment:
+ MYSQL_ROOT_PASSWORD: root
+ MYSQL_DATABASE: peipei_apitest
+ MYSQL_USER: apitest
+ MYSQL_PASSWORD: apitest
+ ports:
+ - "33306:3306"
+ volumes:
+ - ./apitest-mysql/init:/docker-entrypoint-initdb.d:ro
+ healthcheck:
+ test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+ command:
+ - "--default-authentication-plugin=mysql_native_password"
+ - "--lower_case_table_names=1"
+ - "--explicit_defaults_for_timestamp=1"
+ - "--character-set-server=utf8mb4"
+ - "--collation-server=utf8mb4_unicode_ci"
diff --git a/docker/apitest-mysql/init/010-grant-performance-schema.sql b/docker/apitest-mysql/init/010-grant-performance-schema.sql
new file mode 100644
index 0000000..3151403
--- /dev/null
+++ b/docker/apitest-mysql/init/010-grant-performance-schema.sql
@@ -0,0 +1,2 @@
+GRANT SELECT ON performance_schema.* TO 'apitest'@'%';
+FLUSH PRIVILEGES;
diff --git a/docker/apitest-mysql/init/README.md b/docker/apitest-mysql/init/README.md
new file mode 100644
index 0000000..164e71a
--- /dev/null
+++ b/docker/apitest-mysql/init/README.md
@@ -0,0 +1,10 @@
+# API Test MySQL Seed Files
+
+将初始化 schema 和种子数据的 SQL 文件放在此目录下,文件会在 `mysql-apitest` 容器启动时自动执行。
+
+推荐约定:
+- `000-schema.sql`:创建数据库/表结构(可复用 Flyway 生成的整库脚本)。
+- `100-seed-*.sql`:插入基础租户、用户、商品、优惠券等测试数据。
+- `900-cleanup.sql`:可选的清理脚本,用于重置状态。
+
+容器销毁(`docker-compose down -v`)后数据会一起删除,保证每次测试环境一致。
diff --git a/play-admin/pom.xml b/play-admin/pom.xml
index 8938048..c846fcc 100644
--- a/play-admin/pom.xml
+++ b/play-admin/pom.xml
@@ -16,6 +16,7 @@
11
11
UTF-8
+ test
@@ -173,6 +174,22 @@
json-path
test
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.junit.vintage
+ junit-vintage-engine
+
+
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
@@ -203,6 +220,9 @@
3.0.0-M7
false
+
+ ${spring.profiles.active}
+
diff --git a/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java b/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java
new file mode 100644
index 0000000..d3ef6bc
--- /dev/null
+++ b/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java
@@ -0,0 +1,318 @@
+package com.starry.admin.common.apitest;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.starry.admin.modules.clerk.module.entity.PlayClerkCommodityEntity;
+import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
+import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
+import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
+import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
+import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
+import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
+import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
+import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
+import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
+import com.starry.admin.modules.shop.module.entity.PlayCommodityInfoEntity;
+import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
+import com.starry.admin.modules.system.module.entity.SysTenantEntity;
+import com.starry.admin.modules.system.module.entity.SysTenantPackageEntity;
+import com.starry.admin.modules.system.module.entity.SysUserEntity;
+import com.starry.admin.modules.system.service.ISysTenantPackageService;
+import com.starry.admin.modules.system.service.ISysTenantService;
+import com.starry.admin.modules.system.service.SysUserService;
+import com.starry.admin.utils.SecurityUtils;
+import com.starry.common.context.CustomSecurityContextHolder;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.Objects;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+@Profile("apitest")
+public class ApiTestDataSeeder implements CommandLineRunner {
+
+ private static final Logger log = LoggerFactory.getLogger(ApiTestDataSeeder.class);
+
+ public static final String DEFAULT_PACKAGE_ID = "pkg-basic";
+ public static final String DEFAULT_TENANT_ID = "tenant-apitest";
+ public static final String DEFAULT_TENANT_KEY = "tenant-key-apitest";
+ public static final String DEFAULT_TENANT_NAME = "API Test Tenant";
+ public static final String DEFAULT_ADMIN_USER_ID = "user-apitest-admin";
+ public static final String DEFAULT_ADMIN_USERNAME = "apitest-admin";
+ public static final String DEFAULT_GROUP_ID = "group-basic";
+ public static final String DEFAULT_CLERK_LEVEL_ID = "lvl-basic";
+ public static final String DEFAULT_CLERK_ID = "clerk-apitest";
+ public static final String DEFAULT_CLERK_OPEN_ID = "openid-clerk-apitest";
+ public static final String DEFAULT_COMMODITY_ID = "svc-basic";
+ public static final String DEFAULT_CLERK_COMMODITY_ID = "clerk-svc-basic";
+ public static final String DEFAULT_CUSTOMER_ID = "customer-apitest";
+
+ private final ISysTenantPackageService tenantPackageService;
+ private final ISysTenantService tenantService;
+ private final SysUserService sysUserService;
+ private final IPlayPersonnelGroupInfoService personnelGroupInfoService;
+ private final IPlayClerkLevelInfoService clerkLevelInfoService;
+ private final IPlayClerkUserInfoService clerkUserInfoService;
+ private final IPlayCommodityInfoService commodityInfoService;
+ private final IPlayClerkCommodityService clerkCommodityService;
+ private final IPlayCustomUserInfoService customUserInfoService;
+ private final PasswordEncoder passwordEncoder;
+
+ public ApiTestDataSeeder(
+ ISysTenantPackageService tenantPackageService,
+ ISysTenantService tenantService,
+ SysUserService sysUserService,
+ IPlayPersonnelGroupInfoService personnelGroupInfoService,
+ IPlayClerkLevelInfoService clerkLevelInfoService,
+ IPlayClerkUserInfoService clerkUserInfoService,
+ IPlayCommodityInfoService commodityInfoService,
+ IPlayClerkCommodityService clerkCommodityService,
+ IPlayCustomUserInfoService customUserInfoService,
+ PasswordEncoder passwordEncoder) {
+ this.tenantPackageService = tenantPackageService;
+ this.tenantService = tenantService;
+ this.sysUserService = sysUserService;
+ this.personnelGroupInfoService = personnelGroupInfoService;
+ this.clerkLevelInfoService = clerkLevelInfoService;
+ this.clerkUserInfoService = clerkUserInfoService;
+ this.commodityInfoService = commodityInfoService;
+ this.clerkCommodityService = clerkCommodityService;
+ this.customUserInfoService = customUserInfoService;
+ this.passwordEncoder = passwordEncoder;
+ }
+
+ @Override
+ @Transactional
+ public void run(String... args) {
+ seedTenantPackage();
+ seedTenant();
+
+ String originalTenant = SecurityUtils.getTenantId();
+ try {
+ SecurityUtils.setTenantId(DEFAULT_TENANT_ID);
+ seedAdminUser();
+ seedPersonnelGroup();
+ seedClerkLevel();
+ seedCommodity();
+ seedClerk();
+ seedClerkCommodity();
+ seedCustomer();
+ } finally {
+ if (Objects.nonNull(originalTenant)) {
+ SecurityUtils.setTenantId(originalTenant);
+ }
+ CustomSecurityContextHolder.remove();
+ }
+ }
+
+ private void seedTenantPackage() {
+ long existing = tenantPackageService.count(Wrappers.lambdaQuery()
+ .eq(SysTenantPackageEntity::getPackageId, DEFAULT_PACKAGE_ID));
+ if (existing > 0) {
+ log.info("API test tenant package {} already exists", DEFAULT_PACKAGE_ID);
+ return;
+ }
+
+ SysTenantPackageEntity entity = new SysTenantPackageEntity();
+ entity.setPackageId(DEFAULT_PACKAGE_ID);
+ entity.setPackageName("API测试基础套餐");
+ entity.setStatus("0");
+ entity.setMenuIds("[]");
+ entity.setRemarks("Seeded for API integration tests");
+ tenantPackageService.save(entity);
+ log.info("Inserted API test tenant package {}", DEFAULT_PACKAGE_ID);
+ }
+
+ private void seedTenant() {
+ SysTenantEntity tenant = tenantService.getById(DEFAULT_TENANT_ID);
+ if (tenant != null) {
+ log.info("API test tenant {} already exists", DEFAULT_TENANT_ID);
+ return;
+ }
+
+ SysTenantEntity entity = new SysTenantEntity();
+ entity.setTenantId(DEFAULT_TENANT_ID);
+ entity.setTenantName(DEFAULT_TENANT_NAME);
+ entity.setTenantType("0");
+ entity.setTenantStatus("0");
+ entity.setTenantCode("apitest");
+ entity.setTenantKey(DEFAULT_TENANT_KEY);
+ entity.setPackageId(DEFAULT_PACKAGE_ID);
+ entity.setTenantTime(new Date(System.currentTimeMillis() + 365L * 24 * 3600 * 1000));
+ entity.setUserName(DEFAULT_ADMIN_USERNAME);
+ entity.setUserPwd(passwordEncoder.encode("apitest-secret"));
+ entity.setPhone("13800000000");
+ entity.setEmail("apitest@example.com");
+ entity.setAddress("API Test Street 1");
+ tenantService.save(entity);
+ log.info("Inserted API test tenant {}", DEFAULT_TENANT_ID);
+ }
+
+ private void seedAdminUser() {
+ SysUserEntity existing = sysUserService.getById(DEFAULT_ADMIN_USER_ID);
+ if (existing != null) {
+ log.info("API test admin user {} already exists", DEFAULT_ADMIN_USER_ID);
+ return;
+ }
+
+ SysUserEntity admin = new SysUserEntity();
+ admin.setUserId(DEFAULT_ADMIN_USER_ID);
+ admin.setUserCode(DEFAULT_ADMIN_USERNAME);
+ admin.setPassWord(passwordEncoder.encode("apitest-secret"));
+ admin.setRealName("API Test Admin");
+ admin.setUserNickname("API Admin");
+ admin.setStatus(0);
+ admin.setUserType(1);
+ admin.setTenantId(DEFAULT_TENANT_ID);
+ admin.setMobile("13800000000");
+ admin.setAddTime(LocalDateTime.now());
+ admin.setSuperAdmin(Boolean.TRUE);
+ sysUserService.save(admin);
+ log.info("Inserted API test admin user {}", DEFAULT_ADMIN_USER_ID);
+ }
+
+ private void seedPersonnelGroup() {
+ PlayPersonnelGroupInfoEntity group = personnelGroupInfoService.getById(DEFAULT_GROUP_ID);
+ if (group != null) {
+ log.info("API test personnel group {} already exists", DEFAULT_GROUP_ID);
+ return;
+ }
+
+ PlayPersonnelGroupInfoEntity entity = new PlayPersonnelGroupInfoEntity();
+ entity.setId(DEFAULT_GROUP_ID);
+ entity.setTenantId(DEFAULT_TENANT_ID);
+ entity.setSysUserId(DEFAULT_ADMIN_USER_ID);
+ entity.setSysUserCode(DEFAULT_ADMIN_USERNAME);
+ entity.setGroupName("测试小组");
+ entity.setLeaderName("API Admin");
+ entity.setAddTime(LocalDateTime.now());
+ personnelGroupInfoService.save(entity);
+ log.info("Inserted API test personnel group {}", DEFAULT_GROUP_ID);
+ }
+
+ private void seedClerkLevel() {
+ PlayClerkLevelInfoEntity level = clerkLevelInfoService.getById(DEFAULT_CLERK_LEVEL_ID);
+ if (level != null) {
+ log.info("API test clerk level {} already exists", DEFAULT_CLERK_LEVEL_ID);
+ return;
+ }
+
+ PlayClerkLevelInfoEntity entity = new PlayClerkLevelInfoEntity();
+ entity.setId(DEFAULT_CLERK_LEVEL_ID);
+ entity.setTenantId(DEFAULT_TENANT_ID);
+ entity.setName("基础等级");
+ entity.setLevel(1);
+ entity.setFirstRegularRatio(60);
+ entity.setNotFirstRegularRatio(50);
+ entity.setFirstRandomRadio(55);
+ entity.setNotFirstRandomRadio(45);
+ entity.setFirstRewardRatio(40);
+ entity.setNotFirstRewardRatio(35);
+ clerkLevelInfoService.save(entity);
+ log.info("Inserted API test clerk level {}", DEFAULT_CLERK_LEVEL_ID);
+ }
+
+ private void seedCommodity() {
+ PlayCommodityInfoEntity commodity = commodityInfoService.getById(DEFAULT_COMMODITY_ID);
+ if (commodity != null) {
+ log.info("API test commodity {} already exists", DEFAULT_COMMODITY_ID);
+ return;
+ }
+
+ PlayCommodityInfoEntity entity = new PlayCommodityInfoEntity();
+ entity.setId(DEFAULT_COMMODITY_ID);
+ entity.setTenantId(DEFAULT_TENANT_ID);
+ entity.setItemType("service");
+ entity.setItemName("60分钟语音陪聊");
+ entity.setServiceDuration("60min");
+ entity.setEnableStace("1");
+ entity.setSort(1);
+ commodityInfoService.save(entity);
+ log.info("Inserted API test commodity {}", DEFAULT_COMMODITY_ID);
+ }
+
+ private void seedClerk() {
+ PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(DEFAULT_CLERK_ID);
+ if (clerk != null) {
+ log.info("API test clerk {} already exists", DEFAULT_CLERK_ID);
+ return;
+ }
+
+ PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
+ entity.setId(DEFAULT_CLERK_ID);
+ entity.setTenantId(DEFAULT_TENANT_ID);
+ entity.setSysUserId(DEFAULT_ADMIN_USER_ID);
+ entity.setOpenid(DEFAULT_CLERK_OPEN_ID);
+ entity.setNickname("小测官");
+ entity.setGroupId(DEFAULT_GROUP_ID);
+ entity.setLevelId(DEFAULT_CLERK_LEVEL_ID);
+ entity.setFixingLevel("1");
+ entity.setSex("2");
+ entity.setPhone("13900000001");
+ entity.setWeiChatCode("apitest-clerk");
+ entity.setAvatar("https://example.com/avatar.png");
+ entity.setAccountBalance(BigDecimal.ZERO);
+ entity.setOnboardingState("1");
+ entity.setListingState("1");
+ entity.setDisplayState("1");
+ entity.setRandomOrderState("1");
+ entity.setClerkState("1");
+ entity.setEntryTime(LocalDateTime.now());
+ clerkUserInfoService.save(entity);
+ log.info("Inserted API test clerk {}", DEFAULT_CLERK_ID);
+ }
+
+ private void seedClerkCommodity() {
+ PlayClerkCommodityEntity mapping = clerkCommodityService.getById(DEFAULT_CLERK_COMMODITY_ID);
+ if (mapping != null) {
+ log.info("API test clerk commodity {} already exists", DEFAULT_CLERK_COMMODITY_ID);
+ return;
+ }
+
+ PlayClerkCommodityEntity entity = new PlayClerkCommodityEntity();
+ entity.setId(DEFAULT_CLERK_COMMODITY_ID);
+ entity.setTenantId(DEFAULT_TENANT_ID);
+ entity.setClerkId(DEFAULT_CLERK_ID);
+ entity.setCommodityId(DEFAULT_COMMODITY_ID);
+ entity.setCommodityName("60分钟语音陪聊");
+ entity.setEnablingState("1");
+ entity.setSort(1);
+ clerkCommodityService.save(entity);
+ log.info("Inserted API test clerk commodity link {}", DEFAULT_CLERK_COMMODITY_ID);
+ }
+
+ private void seedCustomer() {
+ PlayCustomUserInfoEntity customer = customUserInfoService.getById(DEFAULT_CUSTOMER_ID);
+ if (customer != null) {
+ log.info("API test customer {} already exists", DEFAULT_CUSTOMER_ID);
+ return;
+ }
+
+ PlayCustomUserInfoEntity entity = new PlayCustomUserInfoEntity();
+ entity.setId(DEFAULT_CUSTOMER_ID);
+ entity.setTenantId(DEFAULT_TENANT_ID);
+ entity.setOpenid("openid-customer-apitest");
+ entity.setUnionid("unionid-customer-apitest");
+ entity.setNickname("测试顾客");
+ entity.setSex(1);
+ entity.setPhone("13700000002");
+ entity.setWeiChatCode("apitest-customer");
+ entity.setAccountBalance(new BigDecimal("200.00"));
+ entity.setAccumulatedRechargeAmount(new BigDecimal("200.00"));
+ entity.setAccumulatedConsumptionAmount(BigDecimal.ZERO);
+ entity.setAccountState("1");
+ entity.setSubscribeState("1");
+ entity.setPurchaseState("1");
+ entity.setMobilePhoneState("1");
+ entity.setRegistrationTime(new Date());
+ entity.setLastLoginTime(new Date());
+ customUserInfoService.save(entity);
+ log.info("Inserted API test customer {}", DEFAULT_CUSTOMER_ID);
+ }
+}
diff --git a/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityConfig.java b/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityConfig.java
new file mode 100644
index 0000000..7849086
--- /dev/null
+++ b/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityConfig.java
@@ -0,0 +1,49 @@
+package com.starry.admin.common.security.config;
+
+import com.starry.admin.common.security.filter.ApiTestAuthenticationFilter;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+@Profile("apitest")
+@EnableConfigurationProperties(ApiTestSecurityProperties.class)
+public class ApiTestSecurityConfig extends WebSecurityConfigurerAdapter {
+
+ private final ApiTestSecurityProperties properties;
+
+ public ApiTestSecurityConfig(ApiTestSecurityProperties properties) {
+ this.properties = properties;
+ }
+
+ @Bean
+ public ApiTestAuthenticationFilter apiTestAuthenticationFilter() {
+ return new ApiTestAuthenticationFilter(properties);
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http.csrf().disable()
+ .formLogin().disable()
+ .logout().disable()
+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
+ .authorizeRequests().anyRequest().authenticated().and()
+ .addFilterBefore(apiTestAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
+ }
+}
diff --git a/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityProperties.java b/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityProperties.java
new file mode 100644
index 0000000..0f368af
--- /dev/null
+++ b/play-admin/src/main/java/com/starry/admin/common/security/config/ApiTestSecurityProperties.java
@@ -0,0 +1,73 @@
+package com.starry.admin.common.security.config;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "apitest.security")
+public class ApiTestSecurityProperties {
+
+ private String tenantHeader = "X-Tenant";
+ private String userHeader = "X-Test-User";
+ private final Defaults defaults = new Defaults();
+
+ public String getTenantHeader() {
+ return tenantHeader;
+ }
+
+ public void setTenantHeader(String tenantHeader) {
+ this.tenantHeader = tenantHeader;
+ }
+
+ public String getUserHeader() {
+ return userHeader;
+ }
+
+ public void setUserHeader(String userHeader) {
+ this.userHeader = userHeader;
+ }
+
+ public Defaults getDefaults() {
+ return defaults;
+ }
+
+ public static class Defaults {
+
+ private String tenantId = "tenant-apitest";
+ private String userId = "apitest-user";
+ private List roles = new ArrayList<>();
+ private List permissions = new ArrayList<>();
+
+ public String getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(String tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public List getRoles() {
+ return roles;
+ }
+
+ public void setRoles(List roles) {
+ this.roles = roles;
+ }
+
+ public List getPermissions() {
+ return permissions;
+ }
+
+ public void setPermissions(List permissions) {
+ this.permissions = permissions;
+ }
+ }
+}
diff --git a/play-admin/src/main/java/com/starry/admin/common/security/config/SpringSecurityConfig.java b/play-admin/src/main/java/com/starry/admin/common/security/config/SpringSecurityConfig.java
index b2678d0..0badf73 100644
--- a/play-admin/src/main/java/com/starry/admin/common/security/config/SpringSecurityConfig.java
+++ b/play-admin/src/main/java/com/starry/admin/common/security/config/SpringSecurityConfig.java
@@ -12,6 +12,7 @@ import java.util.Set;
import javax.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@@ -31,6 +32,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
+@Profile("!apitest")
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
diff --git a/play-admin/src/main/java/com/starry/admin/common/security/filter/ApiTestAuthenticationFilter.java b/play-admin/src/main/java/com/starry/admin/common/security/filter/ApiTestAuthenticationFilter.java
new file mode 100644
index 0000000..833e5dd
--- /dev/null
+++ b/play-admin/src/main/java/com/starry/admin/common/security/filter/ApiTestAuthenticationFilter.java
@@ -0,0 +1,96 @@
+package com.starry.admin.common.security.filter;
+
+import com.starry.admin.common.domain.LoginUser;
+import com.starry.admin.common.security.config.ApiTestSecurityProperties;
+import com.starry.admin.modules.system.module.entity.SysUserEntity;
+import com.starry.common.constant.SecurityConstants;
+import com.starry.common.context.CustomSecurityContextHolder;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+public class ApiTestAuthenticationFilter extends OncePerRequestFilter {
+
+ private final ApiTestSecurityProperties properties;
+
+ public ApiTestAuthenticationFilter(ApiTestSecurityProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+ String requestedUser = request.getHeader(properties.getUserHeader());
+ String requestedTenant = request.getHeader(properties.getTenantHeader());
+
+ String userId = StringUtils.hasText(requestedUser) ? requestedUser : properties.getDefaults().getUserId();
+ String tenantId = StringUtils.hasText(requestedTenant) ? requestedTenant : properties.getDefaults().getTenantId();
+
+ if (!StringUtils.hasText(userId) || !StringUtils.hasText(tenantId)) {
+ response.setStatus(HttpStatus.UNAUTHORIZED.value());
+ response.setContentType("application/json");
+ response.getWriter().write("{\"code\":401,\"message\":\"Missing test user or tenant header\"}");
+ response.getWriter().flush();
+ return;
+ }
+
+ try {
+ LoginUser loginUser = buildLoginUser(userId, tenantId);
+
+ UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null,
+ Collections.emptyList());
+ authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ CustomSecurityContextHolder.set(SecurityConstants.DETAILS_USER_ID, userId);
+ CustomSecurityContextHolder.set(SecurityConstants.DETAILS_USERNAME, userId);
+ CustomSecurityContextHolder.setTenantId(tenantId);
+ CustomSecurityContextHolder.setPermission(String.join(",", loginUser.getPermissions()));
+
+ filterChain.doFilter(request, response);
+ } finally {
+ CustomSecurityContextHolder.remove();
+ SecurityContextHolder.clearContext();
+ }
+ }
+
+ private LoginUser buildLoginUser(String userId, String tenantId) {
+ SysUserEntity sysUser = new SysUserEntity();
+ sysUser.setUserId(userId);
+ sysUser.setUserCode(userId);
+ sysUser.setRealName(userId);
+ sysUser.setTenantId(tenantId);
+ sysUser.setSuperAdmin(Boolean.FALSE);
+ sysUser.setStatus(0);
+
+ LoginUser loginUser = new LoginUser();
+ loginUser.setUser(sysUser);
+ loginUser.setUserId(userId);
+ loginUser.setUserName(userId);
+ loginUser.setToken("apitest-" + userId + "-" + tenantId);
+ loginUser.setLoginTime(System.currentTimeMillis());
+ loginUser.setExpireTime(System.currentTimeMillis() + 3600_000);
+ loginUser.setTenantEndDate(new Date(System.currentTimeMillis() + 3600_000));
+ loginUser.setTenantStatus(0);
+
+ Set roles = new HashSet<>(properties.getDefaults().getRoles());
+ Set permissions = new HashSet<>(properties.getDefaults().getPermissions());
+ loginUser.setRoles(roles);
+ loginUser.setPermissions(permissions);
+ loginUser.setCurrentRole(roles.stream().findFirst().orElse(null));
+
+ return loginUser;
+ }
+}
diff --git a/play-admin/src/main/resources/application-apitest.yml b/play-admin/src/main/resources/application-apitest.yml
new file mode 100644
index 0000000..116a226
--- /dev/null
+++ b/play-admin/src/main/resources/application-apitest.yml
@@ -0,0 +1,82 @@
+spring:
+ application:
+ name: admin-tenant-apitest
+ flyway:
+ table: admin_flyway_schema_history
+ baseline-on-migrate: true
+ baseline-version: 1
+ enabled: true
+ locations: classpath:db/migration
+ clean-disabled: false
+ validate-on-migrate: false
+ out-of-order: false
+ datasource:
+ type: com.alibaba.druid.pool.DruidDataSource
+ driverClassName: com.mysql.cj.jdbc.Driver
+ url: jdbc:mysql://localhost:33306/peipei_apitest?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true
+ username: apitest
+ password: apitest
+ druid:
+ enable: true
+ db-type: mysql
+ filters: stat,wall
+ max-active: 20
+ initial-size: 1
+ max-wait: 60000
+ min-idle: 1
+ time-between-eviction-runs-millis: 60000
+ min-evictable-idle-time-millis: 300000
+ validation-query: select 'x'
+ test-while-idle: true
+ test-on-borrow: false
+ test-on-return: false
+ pool-prepared-statements: true
+ max-open-prepared-statements: 20
+ web-stat-filter:
+ enabled: false
+ stat-view-servlet:
+ enabled: false
+ redis:
+ host: 127.0.0.1
+ database: 0
+ port: 36379
+ password:
+ timeout: 3000ms
+ task:
+ scheduling:
+ enabled: false
+ execution:
+ shutdown:
+ await-termination: true
+ await-termination-period: 5s
+logging:
+ level:
+ root: info
+ com.starry: debug
+
+jwt:
+ tokenHeader: X-Test-Token
+ tokenHead: Bearer
+ secret: apitest-secret
+ expiration: 360000
+
+token:
+ header: Authorization
+ secret: apitest-override-secret
+ expireTime: 60
+
+xl:
+ login:
+ authCode:
+ enable: false
+
+apitest:
+ security:
+ tenant-header: X-Tenant
+ user-header: X-Test-User
+ defaults:
+ tenant-id: tenant-apitest
+ user-id: apitest-user
+ roles:
+ - ROLE_TESTER
+ permissions: []
diff --git a/play-admin/src/test/java/com/starry/admin/api/AbstractApiTest.java b/play-admin/src/test/java/com/starry/admin/api/AbstractApiTest.java
new file mode 100644
index 0000000..4d47997
--- /dev/null
+++ b/play-admin/src/test/java/com/starry/admin/api/AbstractApiTest.java
@@ -0,0 +1,21 @@
+package com.starry.admin.api;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.web.servlet.MockMvc;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@AutoConfigureMockMvc
+@ActiveProfiles("apitest")
+public abstract class AbstractApiTest {
+
+ protected static final String TENANT_HEADER = "X-Tenant";
+ protected static final String USER_HEADER = "X-Test-User";
+ protected static final String DEFAULT_TENANT = "tenant-apitest";
+ protected static final String DEFAULT_USER = "apitest-user";
+
+ @Autowired
+ protected MockMvc mockMvc;
+}
diff --git a/play-admin/src/test/java/com/starry/admin/api/HealthControllerApiTest.java b/play-admin/src/test/java/com/starry/admin/api/HealthControllerApiTest.java
new file mode 100644
index 0000000..ec98142
--- /dev/null
+++ b/play-admin/src/test/java/com/starry/admin/api/HealthControllerApiTest.java
@@ -0,0 +1,20 @@
+package com.starry.admin.api;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.jupiter.api.Test;
+
+class HealthControllerApiTest extends AbstractApiTest {
+
+ @Test
+ void pingReturnsPong() throws Exception {
+ mockMvc.perform(get("/health/ping")
+ .header(USER_HEADER, DEFAULT_USER)
+ .header(TENANT_HEADER, DEFAULT_TENANT))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data").value("pong"));
+ }
+}
diff --git a/play-admin/src/test/java/com/starry/admin/api/SysTenantPackageControllerApiTest.java b/play-admin/src/test/java/com/starry/admin/api/SysTenantPackageControllerApiTest.java
new file mode 100644
index 0000000..1e1fa1e
--- /dev/null
+++ b/play-admin/src/test/java/com/starry/admin/api/SysTenantPackageControllerApiTest.java
@@ -0,0 +1,22 @@
+package com.starry.admin.api;
+
+import static org.hamcrest.Matchers.hasItem;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.junit.jupiter.api.Test;
+
+class SysTenantPackageControllerApiTest extends AbstractApiTest {
+
+ @Test
+ void getSimpleListReturnsSeededPackage() throws Exception {
+ mockMvc.perform(get("/platform/package/get-simple-list")
+ .header(USER_HEADER, DEFAULT_USER)
+ .header(TENANT_HEADER, DEFAULT_TENANT))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data").isArray())
+ .andExpect(jsonPath("$.data[*].id", hasItem("pkg-basic")));
+ }
+}
diff --git a/pom.xml b/pom.xml
index 285fc3f..bc17f04 100644
--- a/pom.xml
+++ b/pom.xml
@@ -333,6 +333,14 @@
+
+ apitest
+
+ apitest
+ 33306
+
+
+
osx-arm64
@@ -369,4 +377,4 @@
-
\ No newline at end of file
+