Merge branch 'api-test-1'
Some checks failed
Build and Push Backend / docker (push) Failing after 5s
Some checks failed
Build and Push Backend / docker (push) Failing after 5s
API test first run
This commit is contained in:
27
docker/apitest-mysql.yml
Normal file
27
docker/apitest-mysql.yml
Normal file
@@ -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"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
GRANT SELECT ON performance_schema.* TO 'apitest'@'%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
10
docker/apitest-mysql/init/README.md
Normal file
10
docker/apitest-mysql/init/README.md
Normal file
@@ -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`)后数据会一起删除,保证每次测试环境一致。
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
<maven.compiler.source>11</maven.compiler.source>
|
<maven.compiler.source>11</maven.compiler.source>
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
<maven.compiler.target>11</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<spring.profiles.active>test</spring.profiles.active>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -173,6 +174,22 @@
|
|||||||
<artifactId>json-path</artifactId>
|
<artifactId>json-path</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.junit.vintage</groupId>
|
||||||
|
<artifactId>junit-vintage-engine</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
@@ -203,6 +220,9 @@
|
|||||||
<version>3.0.0-M7</version>
|
<version>3.0.0-M7</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<useSystemClassLoader>false</useSystemClassLoader>
|
<useSystemClassLoader>false</useSystemClassLoader>
|
||||||
|
<systemPropertyVariables>
|
||||||
|
<spring.profiles.active>${spring.profiles.active}</spring.profiles.active>
|
||||||
|
</systemPropertyVariables>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
|||||||
@@ -0,0 +1,431 @@
|
|||||||
|
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.PlayCommodityAndLevelInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCommodityInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCommodityAndLevelInfoService;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
|
||||||
|
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.modules.weichat.service.WxTokenService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
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_PARENT_ID = "svc-parent";
|
||||||
|
public static final String DEFAULT_COMMODITY_PARENT_NAME = "语音陪聊服务";
|
||||||
|
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";
|
||||||
|
public static final String DEFAULT_GIFT_ID = "gift-basic";
|
||||||
|
public static final String DEFAULT_GIFT_NAME = "API测试礼物";
|
||||||
|
public static final BigDecimal DEFAULT_COMMODITY_PRICE = new BigDecimal("120.00");
|
||||||
|
private static final String GIFT_TYPE_REGULAR = "1";
|
||||||
|
private static final String GIFT_STATE_ACTIVE = "0";
|
||||||
|
private static final BigDecimal DEFAULT_CUSTOMER_BALANCE = new BigDecimal("200.00");
|
||||||
|
private static final BigDecimal DEFAULT_CUSTOMER_RECHARGE = DEFAULT_CUSTOMER_BALANCE;
|
||||||
|
|
||||||
|
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 IPlayCommodityAndLevelInfoService commodityAndLevelInfoService;
|
||||||
|
private final IPlayGiftInfoService giftInfoService;
|
||||||
|
private final IPlayClerkCommodityService clerkCommodityService;
|
||||||
|
private final IPlayCustomUserInfoService customUserInfoService;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final WxTokenService wxTokenService;
|
||||||
|
|
||||||
|
public ApiTestDataSeeder(
|
||||||
|
ISysTenantPackageService tenantPackageService,
|
||||||
|
ISysTenantService tenantService,
|
||||||
|
SysUserService sysUserService,
|
||||||
|
IPlayPersonnelGroupInfoService personnelGroupInfoService,
|
||||||
|
IPlayClerkLevelInfoService clerkLevelInfoService,
|
||||||
|
IPlayClerkUserInfoService clerkUserInfoService,
|
||||||
|
IPlayCommodityInfoService commodityInfoService,
|
||||||
|
IPlayCommodityAndLevelInfoService commodityAndLevelInfoService,
|
||||||
|
IPlayGiftInfoService giftInfoService,
|
||||||
|
IPlayClerkCommodityService clerkCommodityService,
|
||||||
|
IPlayCustomUserInfoService customUserInfoService,
|
||||||
|
PasswordEncoder passwordEncoder,
|
||||||
|
WxTokenService wxTokenService) {
|
||||||
|
this.tenantPackageService = tenantPackageService;
|
||||||
|
this.tenantService = tenantService;
|
||||||
|
this.sysUserService = sysUserService;
|
||||||
|
this.personnelGroupInfoService = personnelGroupInfoService;
|
||||||
|
this.clerkLevelInfoService = clerkLevelInfoService;
|
||||||
|
this.clerkUserInfoService = clerkUserInfoService;
|
||||||
|
this.commodityInfoService = commodityInfoService;
|
||||||
|
this.commodityAndLevelInfoService = commodityAndLevelInfoService;
|
||||||
|
this.giftInfoService = giftInfoService;
|
||||||
|
this.clerkCommodityService = clerkCommodityService;
|
||||||
|
this.customUserInfoService = customUserInfoService;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.wxTokenService = wxTokenService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void run(String... args) {
|
||||||
|
seedTenantPackage();
|
||||||
|
seedTenant();
|
||||||
|
|
||||||
|
String originalTenant = SecurityUtils.getTenantId();
|
||||||
|
try {
|
||||||
|
SecurityUtils.setTenantId(DEFAULT_TENANT_ID);
|
||||||
|
seedAdminUser();
|
||||||
|
seedPersonnelGroup();
|
||||||
|
seedClerkLevel();
|
||||||
|
PlayCommodityInfoEntity commodity = seedCommodityHierarchy();
|
||||||
|
seedCommodityPricing(commodity);
|
||||||
|
seedClerk();
|
||||||
|
seedClerkCommodity();
|
||||||
|
seedGift();
|
||||||
|
seedCustomer();
|
||||||
|
} finally {
|
||||||
|
if (Objects.nonNull(originalTenant)) {
|
||||||
|
SecurityUtils.setTenantId(originalTenant);
|
||||||
|
}
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seedTenantPackage() {
|
||||||
|
long existing = tenantPackageService.count(Wrappers.<SysTenantPackageEntity>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 PlayCommodityInfoEntity seedCommodityHierarchy() {
|
||||||
|
PlayCommodityInfoEntity parent = commodityInfoService.getById(DEFAULT_COMMODITY_PARENT_ID);
|
||||||
|
if (parent == null) {
|
||||||
|
parent = new PlayCommodityInfoEntity();
|
||||||
|
parent.setId(DEFAULT_COMMODITY_PARENT_ID);
|
||||||
|
parent.setTenantId(DEFAULT_TENANT_ID);
|
||||||
|
parent.setPId("00");
|
||||||
|
parent.setItemType("service-category");
|
||||||
|
parent.setItemName(DEFAULT_COMMODITY_PARENT_NAME);
|
||||||
|
parent.setEnableStace("1");
|
||||||
|
parent.setSort(1);
|
||||||
|
commodityInfoService.save(parent);
|
||||||
|
log.info("Inserted API test commodity parent {}", DEFAULT_COMMODITY_PARENT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayCommodityInfoEntity child = commodityInfoService.getById(DEFAULT_COMMODITY_ID);
|
||||||
|
if (child != null) {
|
||||||
|
log.info("API test commodity {} already exists", DEFAULT_COMMODITY_ID);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
child = new PlayCommodityInfoEntity();
|
||||||
|
child.setId(DEFAULT_COMMODITY_ID);
|
||||||
|
child.setTenantId(DEFAULT_TENANT_ID);
|
||||||
|
child.setPId(DEFAULT_COMMODITY_PARENT_ID);
|
||||||
|
child.setItemType("service");
|
||||||
|
child.setItemName("60分钟语音陪聊");
|
||||||
|
child.setServiceDuration("60min");
|
||||||
|
child.setEnableStace("1");
|
||||||
|
child.setSort(1);
|
||||||
|
commodityInfoService.save(child);
|
||||||
|
log.info("Inserted API test commodity {}", DEFAULT_COMMODITY_ID);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seedCommodityPricing(PlayCommodityInfoEntity commodity) {
|
||||||
|
if (commodity == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayCommodityAndLevelInfoEntity existing = commodityAndLevelInfoService.lambdaQuery()
|
||||||
|
.eq(PlayCommodityAndLevelInfoEntity::getCommodityId, commodity.getId())
|
||||||
|
.eq(PlayCommodityAndLevelInfoEntity::getLevelId, DEFAULT_CLERK_LEVEL_ID)
|
||||||
|
.one();
|
||||||
|
if (existing != null) {
|
||||||
|
log.info("API test commodity pricing for {} already exists", commodity.getId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayCommodityAndLevelInfoEntity price = new PlayCommodityAndLevelInfoEntity();
|
||||||
|
price.setId(IdUtils.getUuid());
|
||||||
|
price.setTenantId(DEFAULT_TENANT_ID);
|
||||||
|
price.setCommodityId(commodity.getId());
|
||||||
|
price.setLevelId(DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
price.setPrice(DEFAULT_COMMODITY_PRICE);
|
||||||
|
price.setSort(1L);
|
||||||
|
commodityAndLevelInfoService.save(price);
|
||||||
|
log.info("Inserted API test commodity pricing for {}", commodity.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seedClerk() {
|
||||||
|
PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(DEFAULT_CLERK_ID);
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(DEFAULT_CLERK_ID);
|
||||||
|
if (clerk != null) {
|
||||||
|
clerkUserInfoService.updateTokenById(DEFAULT_CLERK_ID, clerkToken);
|
||||||
|
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.setOnlineState("1");
|
||||||
|
entity.setRandomOrderState("1");
|
||||||
|
entity.setClerkState("1");
|
||||||
|
entity.setEntryTime(LocalDateTime.now());
|
||||||
|
entity.setToken(clerkToken);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
String commodityName = DEFAULT_COMMODITY_PARENT_NAME;
|
||||||
|
PlayCommodityInfoEntity parent = commodityInfoService.getById(DEFAULT_COMMODITY_PARENT_ID);
|
||||||
|
if (parent != null && parent.getItemName() != null) {
|
||||||
|
commodityName = parent.getItemName();
|
||||||
|
}
|
||||||
|
|
||||||
|
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(commodityName);
|
||||||
|
entity.setEnablingState("1");
|
||||||
|
entity.setSort(1);
|
||||||
|
clerkCommodityService.save(entity);
|
||||||
|
log.info("Inserted API test clerk commodity link {}", DEFAULT_CLERK_COMMODITY_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seedGift() {
|
||||||
|
PlayGiftInfoEntity gift = giftInfoService.getById(DEFAULT_GIFT_ID);
|
||||||
|
if (gift != null) {
|
||||||
|
log.info("API test gift {} already exists", DEFAULT_GIFT_ID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
|
||||||
|
entity.setId(DEFAULT_GIFT_ID);
|
||||||
|
entity.setTenantId(DEFAULT_TENANT_ID);
|
||||||
|
entity.setHistory("0");
|
||||||
|
entity.setName(DEFAULT_GIFT_NAME);
|
||||||
|
entity.setType(GIFT_TYPE_REGULAR);
|
||||||
|
entity.setUrl("https://example.com/apitest/gift.png");
|
||||||
|
entity.setPrice(new BigDecimal("15.00"));
|
||||||
|
entity.setUnit("CNY");
|
||||||
|
entity.setState(GIFT_STATE_ACTIVE);
|
||||||
|
entity.setListingTime(LocalDateTime.now());
|
||||||
|
entity.setRemark("Seeded gift for API tests");
|
||||||
|
giftInfoService.save(entity);
|
||||||
|
log.info("Inserted API test gift {}", DEFAULT_GIFT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seedCustomer() {
|
||||||
|
PlayCustomUserInfoEntity customer = customUserInfoService.getById(DEFAULT_CUSTOMER_ID);
|
||||||
|
String token = wxTokenService.createWxUserToken(DEFAULT_CUSTOMER_ID);
|
||||||
|
if (customer != null) {
|
||||||
|
customUserInfoService.updateTokenById(DEFAULT_CUSTOMER_ID, token);
|
||||||
|
customUserInfoService.lambdaUpdate()
|
||||||
|
.set(PlayCustomUserInfoEntity::getAccountBalance, DEFAULT_CUSTOMER_BALANCE)
|
||||||
|
.set(PlayCustomUserInfoEntity::getAccumulatedRechargeAmount, DEFAULT_CUSTOMER_RECHARGE)
|
||||||
|
.set(PlayCustomUserInfoEntity::getAccumulatedConsumptionAmount, BigDecimal.ZERO)
|
||||||
|
.set(PlayCustomUserInfoEntity::getAccountState, "1")
|
||||||
|
.set(PlayCustomUserInfoEntity::getSubscribeState, "1")
|
||||||
|
.set(PlayCustomUserInfoEntity::getPurchaseState, "1")
|
||||||
|
.set(PlayCustomUserInfoEntity::getMobilePhoneState, "1")
|
||||||
|
.set(PlayCustomUserInfoEntity::getLastLoginTime, new Date())
|
||||||
|
.eq(PlayCustomUserInfoEntity::getId, DEFAULT_CUSTOMER_ID)
|
||||||
|
.update();
|
||||||
|
log.info("API test customer {} already exists, state refreshed", 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(DEFAULT_CUSTOMER_BALANCE);
|
||||||
|
entity.setAccumulatedRechargeAmount(DEFAULT_CUSTOMER_RECHARGE);
|
||||||
|
entity.setAccumulatedConsumptionAmount(BigDecimal.ZERO);
|
||||||
|
entity.setAccountState("1");
|
||||||
|
entity.setSubscribeState("1");
|
||||||
|
entity.setPurchaseState("1");
|
||||||
|
entity.setMobilePhoneState("1");
|
||||||
|
entity.setRegistrationTime(new Date());
|
||||||
|
entity.setLastLoginTime(new Date());
|
||||||
|
entity.setToken(token);
|
||||||
|
customUserInfoService.save(entity);
|
||||||
|
log.info("Inserted API test customer {}", DEFAULT_CUSTOMER_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> roles = new ArrayList<>();
|
||||||
|
private List<String> 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<String> getRoles() {
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoles(List<String> roles) {
|
||||||
|
this.roles = roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getPermissions() {
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPermissions(List<String> permissions) {
|
||||||
|
this.permissions = permissions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import java.util.Set;
|
|||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||||
@@ -31,6 +32,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
|
|||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||||
|
@Profile("!apitest")
|
||||||
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
|
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
|
|||||||
@@ -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<String> roles = new HashSet<>(properties.getDefaults().getRoles());
|
||||||
|
Set<String> permissions = new HashSet<>(properties.getDefaults().getPermissions());
|
||||||
|
loginUser.setRoles(roles);
|
||||||
|
loginUser.setPermissions(permissions);
|
||||||
|
loginUser.setCurrentRole(roles.stream().findFirst().orElse(null));
|
||||||
|
|
||||||
|
return loginUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.starry.admin.modules.blindbox.config;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class BlindBoxConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Clock systemClock() {
|
||||||
|
return Clock.systemDefaultZone();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,11 @@ public class PlayCommodityInfoEntity extends BaseEntity<PlayCommodityInfoEntity>
|
|||||||
*/
|
*/
|
||||||
private String serviceDuration;
|
private String serviceDuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接单后自动结算等待时长(单位:秒,-1 表示不自动结算)
|
||||||
|
*/
|
||||||
|
private Integer automaticSettlementDuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启用状态(0:停用,1:启用)
|
* 启用状态(0:停用,1:启用)
|
||||||
*/
|
*/
|
||||||
|
|||||||
82
play-admin/src/main/resources/application-apitest.yml
Normal file
82
play-admin/src/main/resources/application-apitest.yml
Normal file
@@ -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: []
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.common.exception.CustomException;
|
||||||
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
|
||||||
|
import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper;
|
||||||
|
import com.starry.admin.modules.blindbox.module.constant.BlindBoxConfigStatus;
|
||||||
|
import com.starry.admin.modules.blindbox.module.constant.BlindBoxPoolStatus;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxPoolEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
|
||||||
|
import com.starry.admin.modules.blindbox.service.BlindBoxConfigService;
|
||||||
|
import com.starry.admin.modules.blindbox.service.BlindBoxService;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftHistory;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftState;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.GiftType;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
class BlindBoxServiceWeightTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
|
private static final String TEST_BLIND_BOX_ID = "blindbox-apitest";
|
||||||
|
private static final String PRIMARY_GIFT_ID = ApiTestDataSeeder.DEFAULT_GIFT_ID;
|
||||||
|
private static final String SECONDARY_GIFT_ID = "gift-blindbox-secondary";
|
||||||
|
private static final int DRAW_ATTEMPT_COUNT = 1_000;
|
||||||
|
private static final int PRIMARY_WEIGHT = 80;
|
||||||
|
private static final int SECONDARY_WEIGHT = 20;
|
||||||
|
private static final double PRIMARY_RATIO_MIN = 0.68;
|
||||||
|
private static final double PRIMARY_RATIO_MAX = 0.88;
|
||||||
|
private static final double SECONDARY_RATIO_MIN = 0.12;
|
||||||
|
private static final double SECONDARY_RATIO_MAX = 0.32;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxService blindBoxService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxConfigService blindBoxConfigService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxPoolMapper blindBoxPoolMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BlindBoxRewardMapper blindBoxRewardMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayGiftInfoService giftInfoService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUpTenant() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void clearContext() {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:在默认租户下补齐盲盒配置以及主、副礼物后,以 80/20 的权重写入奖池,
|
||||||
|
// 连续执行 1000 次盲盒抽奖,既校验每次抽奖都有奖励返回且两种礼物都被抽中,
|
||||||
|
// 也验证主礼物命中率位于 68%~88%、副礼物命中率位于 12%~32%,确保抽奖概率符合权重设定。
|
||||||
|
void blindBoxDrawRespectsWeightDistribution() {
|
||||||
|
ensureTenantContext();
|
||||||
|
ensureBlindBoxConfig();
|
||||||
|
ensurePrimaryGift();
|
||||||
|
ensureSecondaryGift();
|
||||||
|
resetCustomerBalance();
|
||||||
|
|
||||||
|
purgeRewards();
|
||||||
|
purgePool();
|
||||||
|
insertPoolEntry(PRIMARY_GIFT_ID, PRIMARY_WEIGHT);
|
||||||
|
insertPoolEntry(SECONDARY_GIFT_ID, SECONDARY_WEIGHT);
|
||||||
|
|
||||||
|
Map<String, Integer> frequency = new HashMap<>();
|
||||||
|
for (int i = 0; i < DRAW_ATTEMPT_COUNT; i++) {
|
||||||
|
BlindBoxRewardEntity reward = blindBoxService.drawReward(
|
||||||
|
ApiTestDataSeeder.DEFAULT_TENANT_ID,
|
||||||
|
"order-" + i,
|
||||||
|
ApiTestDataSeeder.DEFAULT_CUSTOMER_ID,
|
||||||
|
TEST_BLIND_BOX_ID,
|
||||||
|
"seed-" + i);
|
||||||
|
frequency.merge(reward.getRewardGiftId(), 1, Integer::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
int primaryCount = frequency.getOrDefault(PRIMARY_GIFT_ID, 0);
|
||||||
|
int secondaryCount = frequency.getOrDefault(SECONDARY_GIFT_ID, 0);
|
||||||
|
int total = primaryCount + secondaryCount;
|
||||||
|
|
||||||
|
Assertions.assertThat(total).isEqualTo(DRAW_ATTEMPT_COUNT);
|
||||||
|
Assertions.assertThat(primaryCount).isGreaterThan(0);
|
||||||
|
Assertions.assertThat(secondaryCount).isGreaterThan(0);
|
||||||
|
|
||||||
|
double primaryRatio = primaryCount / (double) total;
|
||||||
|
double secondaryRatio = secondaryCount / (double) total;
|
||||||
|
|
||||||
|
Assertions.assertThat(primaryRatio).isBetween(PRIMARY_RATIO_MIN, PRIMARY_RATIO_MAX);
|
||||||
|
Assertions.assertThat(secondaryRatio).isBetween(SECONDARY_RATIO_MIN, SECONDARY_RATIO_MAX);
|
||||||
|
|
||||||
|
purgeRewards();
|
||||||
|
purgePool();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureBlindBoxConfig() {
|
||||||
|
BlindBoxConfigEntity config = blindBoxConfigService.getById(TEST_BLIND_BOX_ID);
|
||||||
|
if (config != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BlindBoxConfigEntity entity = new BlindBoxConfigEntity();
|
||||||
|
entity.setId(TEST_BLIND_BOX_ID);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setName("API盲盒");
|
||||||
|
entity.setCoverUrl("https://example.com/apitest/blindbox-cover.png");
|
||||||
|
entity.setDescription("Seeded blind box for integration tests");
|
||||||
|
entity.setPrice(new BigDecimal("19.90"));
|
||||||
|
entity.setStatus(BlindBoxConfigStatus.ENABLED.getCode());
|
||||||
|
blindBoxConfigService.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureSecondaryGift() {
|
||||||
|
PlayGiftInfoEntity existing = findGift(SECONDARY_GIFT_ID);
|
||||||
|
if (existing != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
|
||||||
|
entity.setId(SECONDARY_GIFT_ID);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setHistory(GiftHistory.CURRENT.getCode());
|
||||||
|
entity.setName("API盲盒奖励");
|
||||||
|
entity.setType(GiftType.NORMAL.getCode());
|
||||||
|
entity.setUrl("https://example.com/apitest/blindbox.png");
|
||||||
|
entity.setPrice(new BigDecimal("9.99"));
|
||||||
|
entity.setUnit("CNY");
|
||||||
|
entity.setState(GiftState.ACTIVE.getCode());
|
||||||
|
entity.setListingTime(LocalDateTime.now());
|
||||||
|
entity.setRemark("Seeded secondary gift for blind box tests");
|
||||||
|
giftInfoService.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensurePrimaryGift() {
|
||||||
|
PlayGiftInfoEntity existing = findGift(PRIMARY_GIFT_ID);
|
||||||
|
if (existing != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlayGiftInfoEntity entity = new PlayGiftInfoEntity();
|
||||||
|
entity.setId(PRIMARY_GIFT_ID);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setHistory(GiftHistory.CURRENT.getCode());
|
||||||
|
entity.setName(ApiTestDataSeeder.DEFAULT_GIFT_NAME);
|
||||||
|
entity.setType(GiftType.NORMAL.getCode());
|
||||||
|
entity.setUrl("https://example.com/apitest/gift-basic.png");
|
||||||
|
entity.setPrice(new BigDecimal("15.00"));
|
||||||
|
entity.setUnit("CNY");
|
||||||
|
entity.setState(GiftState.ACTIVE.getCode());
|
||||||
|
entity.setListingTime(LocalDateTime.now());
|
||||||
|
entity.setRemark("Seeded default gift for blind box tests");
|
||||||
|
giftInfoService.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertPoolEntry(String giftId, int weight) {
|
||||||
|
PlayGiftInfoEntity gift = findGift(giftId);
|
||||||
|
if (gift == null) {
|
||||||
|
throw new IllegalStateException("Expected gift to be seeded: " + giftId);
|
||||||
|
}
|
||||||
|
BlindBoxPoolEntity entry = new BlindBoxPoolEntity();
|
||||||
|
entry.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entry.setBlindBoxId(TEST_BLIND_BOX_ID);
|
||||||
|
entry.setRewardGiftId(giftId);
|
||||||
|
entry.setRewardPrice(gift.getPrice());
|
||||||
|
entry.setWeight(weight);
|
||||||
|
entry.setRemainingStock(null);
|
||||||
|
entry.setValidFrom(null);
|
||||||
|
entry.setValidTo(null);
|
||||||
|
entry.setStatus(BlindBoxPoolStatus.ENABLED.getCode());
|
||||||
|
blindBoxPoolMapper.insert(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayGiftInfoEntity findGift(String giftId) {
|
||||||
|
try {
|
||||||
|
return giftInfoService.selectPlayGiftInfoById(giftId);
|
||||||
|
} catch (CustomException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void purgePool() {
|
||||||
|
blindBoxPoolMapper.delete(new LambdaQueryWrapper<BlindBoxPoolEntity>()
|
||||||
|
.eq(BlindBoxPoolEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
|
||||||
|
.eq(BlindBoxPoolEntity::getBlindBoxId, TEST_BLIND_BOX_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void purgeRewards() {
|
||||||
|
blindBoxRewardMapper.delete(new LambdaQueryWrapper<BlindBoxRewardEntity>()
|
||||||
|
.eq(BlindBoxRewardEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
|
||||||
|
.eq(BlindBoxRewardEntity::getBlindBoxId, TEST_BLIND_BOX_ID)
|
||||||
|
.eq(BlindBoxRewardEntity::getCustomerId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
|
// 测试用例:模拟带上默认用户与租户请求 /health/ping 接口,校验接口必须返回 200 且 data = "pong",确认健康检查链路畅通。
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,519 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkCommodityEntity;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkCommodityService;
|
||||||
|
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCommodityAndLevelInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCommodityInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCommodityAndLevelInfoService;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCommodityInfoService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class PlayCommodityInfoApiTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
|
private static final String ROOT_PARENT_ID = "00";
|
||||||
|
private static final long DEFAULT_PRICE_SORT = 1L;
|
||||||
|
private static final int SINGLE_COMMODITY_QUANTITY = 1;
|
||||||
|
|
||||||
|
private enum SwitchState {
|
||||||
|
ENABLED("1"),
|
||||||
|
DISABLED("0");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
SwitchState(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum HistoryFilter {
|
||||||
|
INCLUDE("0"),
|
||||||
|
EXCLUDE("1");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
HistoryFilter(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SortPriority {
|
||||||
|
PRIMARY(1),
|
||||||
|
SECONDARY(2);
|
||||||
|
|
||||||
|
private final int value;
|
||||||
|
|
||||||
|
SortPriority(int value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum AutomaticSettlementPolicy {
|
||||||
|
DISABLED(-1),
|
||||||
|
TEN_MINUTES(600);
|
||||||
|
|
||||||
|
private final int seconds;
|
||||||
|
|
||||||
|
AutomaticSettlementPolicy(int seconds) {
|
||||||
|
this.seconds = seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getSeconds() {
|
||||||
|
return seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayCommodityInfoService commodityInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayCommodityAndLevelInfoService commodityAndLevelInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkCommodityService clerkCommodityService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkLevelInfoService clerkLevelInfoService;
|
||||||
|
|
||||||
|
private final Deque<String> commodityIdsToCleanup = new ArrayDeque<>();
|
||||||
|
private final List<String> priceIdsToCleanup = new ArrayList<>();
|
||||||
|
private final List<String> clerkCommodityIdsToCleanup = new ArrayList<>();
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
ensureTenantContext();
|
||||||
|
if (!priceIdsToCleanup.isEmpty()) {
|
||||||
|
commodityAndLevelInfoService.removeByIds(priceIdsToCleanup);
|
||||||
|
priceIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
if (!clerkCommodityIdsToCleanup.isEmpty()) {
|
||||||
|
clerkCommodityService.removeByIds(clerkCommodityIdsToCleanup);
|
||||||
|
clerkCommodityIdsToCleanup.clear();
|
||||||
|
}
|
||||||
|
while (!commodityIdsToCleanup.isEmpty()) {
|
||||||
|
String commodityId = commodityIdsToCleanup.removeFirst();
|
||||||
|
commodityInfoService.removeById(commodityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:调用商品表头接口,验证默认店员等级会出现在表头列表中,确保管理端可正确展示等级价格列。
|
||||||
|
void tableNameEndpointReturnsClerkLevels() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayClerkLevelInfoEntity level =
|
||||||
|
clerkLevelInfoService.getById(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/shop/commodity/getTableName")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data[*].prop", hasItem(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID)))
|
||||||
|
.andExpect(jsonPath("$.data[*].name", hasItem(level.getName())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:查询所有商品树,验证种子数据的父子节点与对应等级价格一起返回,确保层级结构与定价展示正确。
|
||||||
|
void listAllIncludesSeedCommodityWithLevelPrice() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
MvcResult result = mockMvc.perform(get("/shop/commodity/listAll")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
|
JsonNode data = root.get("data");
|
||||||
|
assertThat(data.isArray()).isTrue();
|
||||||
|
|
||||||
|
JsonNode parentNode = findNodeById(data, ApiTestDataSeeder.DEFAULT_COMMODITY_PARENT_ID);
|
||||||
|
assertThat(parentNode).isNotNull();
|
||||||
|
|
||||||
|
JsonNode children = parentNode.get("children");
|
||||||
|
JsonNode childNode = findNodeById(children, ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
assertThat(childNode).isNotNull();
|
||||||
|
|
||||||
|
BigDecimal price = new BigDecimal(childNode.get(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID).asText());
|
||||||
|
assertThat(price).isEqualByComparingTo(ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:调用价格更新接口,为新创建的子商品配置等级价格,验证价格落库并可复查,确保后台配置生效。
|
||||||
|
void updateInfoAssignsLevelPricing() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String parentId = generateId("svc-parent-");
|
||||||
|
String childId = generateId("svc-child-");
|
||||||
|
|
||||||
|
PlayCommodityInfoEntity parent = createCommodity(
|
||||||
|
parentId,
|
||||||
|
ROOT_PARENT_ID,
|
||||||
|
"API测试父类目",
|
||||||
|
"N/A",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.PRIMARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
PlayCommodityInfoEntity child = createCommodity(
|
||||||
|
childId,
|
||||||
|
parent.getId(),
|
||||||
|
"API测试子项目",
|
||||||
|
"45min",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.PRIMARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("id", child.getId());
|
||||||
|
payload.put(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID, "188.50");
|
||||||
|
|
||||||
|
mockMvc.perform(post("/shop/commodity/updateInfo")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
PlayCommodityAndLevelInfoEntity pricing = commodityAndLevelInfoService.lambdaQuery()
|
||||||
|
.eq(PlayCommodityAndLevelInfoEntity::getCommodityId, child.getId())
|
||||||
|
.eq(PlayCommodityAndLevelInfoEntity::getLevelId, ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID)
|
||||||
|
.one();
|
||||||
|
|
||||||
|
assertThat(pricing).isNotNull();
|
||||||
|
priceIdsToCleanup.add(pricing.getId());
|
||||||
|
assertThat(pricing.getPrice()).isEqualByComparingTo(new BigDecimal("188.50"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:使用商品修改接口把自动结算等待时长从不限时(-1)调整为10分钟,验证更新后查询返回新的配置。
|
||||||
|
void updateEndpointSwitchesAutomaticSettlement() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String parentId = generateId("svc-parent-");
|
||||||
|
String childId = generateId("svc-child-");
|
||||||
|
|
||||||
|
PlayCommodityInfoEntity parent = createCommodity(
|
||||||
|
parentId,
|
||||||
|
ROOT_PARENT_ID,
|
||||||
|
"自动结算父类目",
|
||||||
|
"N/A",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.PRIMARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
PlayCommodityInfoEntity child = createCommodity(
|
||||||
|
childId,
|
||||||
|
parent.getId(),
|
||||||
|
"自动结算子项",
|
||||||
|
"30min",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.PRIMARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
|
||||||
|
PlayCommodityInfoEntity beforeUpdate = commodityInfoService.getById(child.getId());
|
||||||
|
assertThat(beforeUpdate.getAutomaticSettlementDuration())
|
||||||
|
.isEqualTo(AutomaticSettlementPolicy.DISABLED.getSeconds());
|
||||||
|
|
||||||
|
PlayCommodityInfoEntity updateBody = new PlayCommodityInfoEntity();
|
||||||
|
updateBody.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
updateBody.setPId(parent.getId());
|
||||||
|
updateBody.setItemType(child.getItemType());
|
||||||
|
updateBody.setItemName(child.getItemName());
|
||||||
|
updateBody.setServiceDuration("30min");
|
||||||
|
updateBody.setAutomaticSettlementDuration(AutomaticSettlementPolicy.TEN_MINUTES.getSeconds());
|
||||||
|
updateBody.setEnableStace(SwitchState.ENABLED.getCode());
|
||||||
|
updateBody.setSort(SortPriority.PRIMARY.getValue());
|
||||||
|
|
||||||
|
mockMvc.perform(post("/shop/commodity/update/" + child.getId())
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(objectMapper.writeValueAsString(updateBody)))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
PlayCommodityInfoEntity updated = commodityInfoService.getById(child.getId());
|
||||||
|
assertThat(updated.getAutomaticSettlementDuration())
|
||||||
|
.isEqualTo(AutomaticSettlementPolicy.TEN_MINUTES.getSeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:为店员构造包含定价与未定价子项的类目,调用按等级查询接口,验证未设置价格的子项被过滤,只返回可售商品。
|
||||||
|
void queryClerkAllCommodityByLevelSkipsUnpricedChildren() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String parentId = generateId("svc-parent-");
|
||||||
|
String pricedChildId = generateId("svc-child-priced-");
|
||||||
|
String unpricedChildId = generateId("svc-child-unpriced-");
|
||||||
|
|
||||||
|
PlayCommodityInfoEntity parent = createCommodity(
|
||||||
|
parentId,
|
||||||
|
ROOT_PARENT_ID,
|
||||||
|
"等级过滤父类目",
|
||||||
|
"N/A",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.SECONDARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
PlayCommodityInfoEntity pricedChild = createCommodity(
|
||||||
|
pricedChildId,
|
||||||
|
parent.getId(),
|
||||||
|
"有定价子项",
|
||||||
|
"60min",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.PRIMARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
createCommodity(
|
||||||
|
unpricedChildId,
|
||||||
|
parent.getId(),
|
||||||
|
"无定价子项",
|
||||||
|
"60min",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.SECONDARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
|
||||||
|
PlayCommodityAndLevelInfoEntity price = createPrice(pricedChild.getId(), new BigDecimal("99.90"));
|
||||||
|
linkCommodityToClerk(parent, SwitchState.ENABLED, SortPriority.PRIMARY);
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/wx/commodity/custom/queryClerkAllCommodityByLevel")
|
||||||
|
.param("id", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
|
JsonNode data = root.get("data");
|
||||||
|
JsonNode parentNode = findNodeById(data, parent.getId());
|
||||||
|
assertThat(parentNode).isNotNull();
|
||||||
|
|
||||||
|
JsonNode children = parentNode.get("child");
|
||||||
|
assertThat(children.isArray()).isTrue();
|
||||||
|
JsonNode pricedNode = findNodeById(children, pricedChild.getId());
|
||||||
|
assertThat(pricedNode).isNotNull();
|
||||||
|
assertThat(pricedNode.get("price").decimalValue()).isEqualByComparingTo(new BigDecimal("99.90"));
|
||||||
|
|
||||||
|
JsonNode removedNode = findNodeById(children, unpricedChildId);
|
||||||
|
assertThat(removedNode).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:为店员同时配置上架与下架的类目,调用顾客查询接口,确认停用类目被隐藏,避免顾客看到不可售项目。
|
||||||
|
void customerCommodityQueryFiltersDisabledParents() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String enabledParentId = generateId("svc-enabled-parent-");
|
||||||
|
String disabledParentId = generateId("svc-disabled-parent-");
|
||||||
|
|
||||||
|
PlayCommodityInfoEntity enabledParent = createCommodity(
|
||||||
|
enabledParentId,
|
||||||
|
ROOT_PARENT_ID,
|
||||||
|
"顾客可见父类目",
|
||||||
|
"N/A",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.PRIMARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
PlayCommodityInfoEntity enabledChild = createCommodity(
|
||||||
|
generateId("svc-enabled-child-"),
|
||||||
|
enabledParent.getId(),
|
||||||
|
"顾客可见子项",
|
||||||
|
"30min",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.PRIMARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
PlayCommodityAndLevelInfoEntity enabledPrice = createPrice(enabledChild.getId(), new BigDecimal("45.00"));
|
||||||
|
|
||||||
|
PlayCommodityInfoEntity disabledParent = createCommodity(
|
||||||
|
disabledParentId,
|
||||||
|
ROOT_PARENT_ID,
|
||||||
|
"顾客屏蔽父类目",
|
||||||
|
"N/A",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.SECONDARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
PlayCommodityInfoEntity disabledChild = createCommodity(
|
||||||
|
generateId("svc-disabled-child-"),
|
||||||
|
disabledParent.getId(),
|
||||||
|
"顾客屏蔽子项",
|
||||||
|
"30min",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.PRIMARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
PlayCommodityAndLevelInfoEntity disabledPrice = createPrice(disabledChild.getId(), new BigDecimal("55.00"));
|
||||||
|
|
||||||
|
linkCommodityToClerk(enabledParent, SwitchState.ENABLED, SortPriority.PRIMARY);
|
||||||
|
linkCommodityToClerk(disabledParent, SwitchState.DISABLED, SortPriority.SECONDARY);
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/wx/commodity/custom/queryClerkAllCommodity")
|
||||||
|
.param("id", ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
|
JsonNode data = root.get("data");
|
||||||
|
|
||||||
|
JsonNode enabledNode = findNodeById(data, enabledParent.getId());
|
||||||
|
assertThat(enabledNode).isNotNull();
|
||||||
|
|
||||||
|
JsonNode disabledNode = findNodeById(data, disabledParent.getId());
|
||||||
|
assertThat(disabledNode).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:顾客随机下单选择未配置价格的商品时,接口应返回“服务项目不存在”的业务错误,验证错误商品被拒单。
|
||||||
|
void randomOrderRejectsCommodityWithoutPricing() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
PlayCommodityInfoEntity parent = createCommodity(
|
||||||
|
generateId("svc-parent-"),
|
||||||
|
ROOT_PARENT_ID,
|
||||||
|
"未定价父类目",
|
||||||
|
"N/A",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.PRIMARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
PlayCommodityInfoEntity child = createCommodity(
|
||||||
|
generateId("svc-child-"),
|
||||||
|
parent.getId(),
|
||||||
|
"未定价子项",
|
||||||
|
"45min",
|
||||||
|
SwitchState.ENABLED,
|
||||||
|
SortPriority.PRIMARY,
|
||||||
|
AutomaticSettlementPolicy.DISABLED);
|
||||||
|
|
||||||
|
ObjectNode payload = objectMapper.createObjectNode();
|
||||||
|
payload.put("sex", OrderConstant.Gender.FEMALE.getCode());
|
||||||
|
payload.put("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
payload.put("commodityId", child.getId());
|
||||||
|
payload.put("commodityQuantity", SINGLE_COMMODITY_QUANTITY);
|
||||||
|
payload.put("weiChatCode", "apitest-customer-wx");
|
||||||
|
payload.put("excludeHistory", HistoryFilter.INCLUDE.getCode());
|
||||||
|
payload.putArray("couponIds");
|
||||||
|
payload.put("remark", "未定价拒单校验");
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/random")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload.toString()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(500))
|
||||||
|
.andExpect(jsonPath("$.message").value("服务项目不存在"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode findNodeById(JsonNode arrayNode, String id) {
|
||||||
|
if (arrayNode == null || !arrayNode.isArray()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (JsonNode node : arrayNode) {
|
||||||
|
if (node.has("id") && id.equals(node.get("id").asText())) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayCommodityInfoEntity createCommodity(
|
||||||
|
String id,
|
||||||
|
String parentId,
|
||||||
|
String name,
|
||||||
|
String duration,
|
||||||
|
SwitchState switchState,
|
||||||
|
SortPriority sortPriority,
|
||||||
|
AutomaticSettlementPolicy settlementPolicy) {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCommodityInfoEntity entity = new PlayCommodityInfoEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setPId(parentId);
|
||||||
|
entity.setItemType("api-test");
|
||||||
|
entity.setItemName(name);
|
||||||
|
entity.setServiceDuration(duration);
|
||||||
|
entity.setAutomaticSettlementDuration(settlementPolicy.getSeconds());
|
||||||
|
entity.setEnableStace(switchState.getCode());
|
||||||
|
entity.setSort(sortPriority.getValue());
|
||||||
|
commodityInfoService.save(entity);
|
||||||
|
commodityIdsToCleanup.addFirst(entity.getId());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayCommodityAndLevelInfoEntity createPrice(String commodityId, BigDecimal price) {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCommodityAndLevelInfoEntity entity = new PlayCommodityAndLevelInfoEntity();
|
||||||
|
entity.setId(IdUtils.getUuid());
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setCommodityId(commodityId);
|
||||||
|
entity.setLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
entity.setPrice(price);
|
||||||
|
entity.setSort(DEFAULT_PRICE_SORT);
|
||||||
|
commodityAndLevelInfoService.save(entity);
|
||||||
|
priceIdsToCleanup.add(entity.getId());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayClerkCommodityEntity linkCommodityToClerk(
|
||||||
|
PlayCommodityInfoEntity parent,
|
||||||
|
SwitchState switchState,
|
||||||
|
SortPriority sortPriority) {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayClerkCommodityEntity entity = new PlayClerkCommodityEntity();
|
||||||
|
entity.setId(IdUtils.getUuid());
|
||||||
|
entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
entity.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
entity.setCommodityId(parent.getId());
|
||||||
|
entity.setCommodityName(parent.getItemName());
|
||||||
|
entity.setEnablingState(switchState.getCode());
|
||||||
|
entity.setSort(sortPriority.getValue());
|
||||||
|
clerkCommodityService.save(entity);
|
||||||
|
clerkCommodityIdsToCleanup.add(entity.getId());
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateId(String prefix) {
|
||||||
|
return prefix + IdUtils.getUuid().replace("-", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void ensureTenantContext() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
|
// 测试用例:以默认平台管理员身份拉取套餐精简列表,应返回 200,
|
||||||
|
// 并且数据集中必须包含种子数据 pkg-basic,验证套餐列表接口能正确曝光基础套餐。
|
||||||
|
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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.custom.module.entity.PlayCustomGiftInfoEntity;
|
||||||
|
import com.starry.admin.modules.custom.service.IPlayCustomGiftInfoService;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayClerkGiftInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayGiftInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayGiftInfoService;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
|
class WxCustomGiftOrderApiTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayGiftInfoService playGiftInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayCustomGiftInfoService playCustomGiftInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPlayClerkGiftInfoService playClerkGiftInfoService;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:用户余额充足且携带有效登录态时,请求 /wx/custom/order/gift 下单指定礼物,
|
||||||
|
// 期望生成已完成的礼物奖励订单、产生对应收益记录,同时校验用户/陪玩师礼物计数与账户余额随订单金额同步更新。
|
||||||
|
void giftOrderCreatesCompletedRewardAndUpdatesGiftCounters() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
PlayGiftInfoEntity gift = playGiftInfoService.selectPlayGiftInfoById(ApiTestDataSeeder.DEFAULT_GIFT_ID);
|
||||||
|
Assertions.assertThat(gift).as("seeded gift should exist").isNotNull();
|
||||||
|
|
||||||
|
int giftQuantity = 2;
|
||||||
|
String remark = "API gift order " + IdUtils.getUuid();
|
||||||
|
BigDecimal totalAmount = gift.getPrice().multiply(BigDecimal.valueOf(giftQuantity));
|
||||||
|
BigDecimal initialBalance = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID).getAccountBalance();
|
||||||
|
|
||||||
|
String payload = "{" +
|
||||||
|
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
|
||||||
|
"\"giftId\":\"" + ApiTestDataSeeder.DEFAULT_GIFT_ID + "\"," +
|
||||||
|
"\"giftQuantity\":" + giftQuantity + "," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
String response = mockMvc.perform(post("/wx/custom/order/gift")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").isNotEmpty())
|
||||||
|
.andReturn()
|
||||||
|
.getResponse()
|
||||||
|
.getContentAsString();
|
||||||
|
|
||||||
|
JsonNode root = objectMapper.readTree(response);
|
||||||
|
String orderId = root.path("data").asText();
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = playOrderInfoService.selectOrderInfoById(orderId);
|
||||||
|
Assertions.assertThat(order).isNotNull();
|
||||||
|
Assertions.assertThat(order.getRemark()).isEqualTo(remark);
|
||||||
|
Assertions.assertThat(order.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.COMPLETED.getCode());
|
||||||
|
Assertions.assertThat(order.getPlaceType()).isEqualTo(OrderConstant.PlaceType.REWARD.getCode());
|
||||||
|
Assertions.assertThat(order.getRewardType()).isEqualTo(OrderConstant.RewardType.GIFT.getCode());
|
||||||
|
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(totalAmount);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
long earningsCount = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||||
|
.count();
|
||||||
|
Assertions.assertThat(earningsCount).isEqualTo(1);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCustomGiftInfoEntity customerGift = playCustomGiftInfoService.lambdaQuery()
|
||||||
|
.eq(PlayCustomGiftInfoEntity::getCustomId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayCustomGiftInfoEntity::getGiffId, ApiTestDataSeeder.DEFAULT_GIFT_ID)
|
||||||
|
.one();
|
||||||
|
Assertions.assertThat(customerGift).isNotNull();
|
||||||
|
Assertions.assertThat(customerGift.getGiffNumber()).isEqualTo((long) giftQuantity);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayClerkGiftInfoEntity clerkGift = playClerkGiftInfoService.lambdaQuery()
|
||||||
|
.eq(PlayClerkGiftInfoEntity::getClerkId, ApiTestDataSeeder.DEFAULT_CLERK_ID)
|
||||||
|
.eq(PlayClerkGiftInfoEntity::getGiffId, ApiTestDataSeeder.DEFAULT_GIFT_ID)
|
||||||
|
.one();
|
||||||
|
Assertions.assertThat(clerkGift).isNotNull();
|
||||||
|
Assertions.assertThat(clerkGift.getGiffNumber()).isEqualTo((long) giftQuantity);
|
||||||
|
|
||||||
|
BigDecimal finalBalance = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.getAccountBalance();
|
||||||
|
Assertions.assertThat(finalBalance).isEqualByComparingTo(initialBalance.subtract(totalAmount));
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
||||||
|
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.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant.PlaceType;
|
||||||
|
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponDiscountType;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
||||||
|
import com.starry.admin.modules.weichat.service.WxTokenService;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
|
import com.starry.admin.modules.withdraw.service.IFreezePolicyService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collections;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
abstract class WxCustomOrderApiTestSupport extends AbstractApiTest {
|
||||||
|
|
||||||
|
protected static final DateTimeFormatter DATE_TIME_FORMATTER =
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
protected final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected WxTokenService wxTokenService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected IPlayOrderInfoService playOrderInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected IPlayCustomUserInfoService customUserInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected IPlayClerkUserInfoService clerkUserInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected IEarningsService earningsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected IFreezePolicyService freezePolicyService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected IPlayClerkLevelInfoService clerkLevelInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected IPlayCouponInfoService couponInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected IPlayCouponDetailsService couponDetailsService;
|
||||||
|
|
||||||
|
protected void resetCustomerBalance() {
|
||||||
|
BigDecimal balance = new BigDecimal("200.00");
|
||||||
|
customUserInfoService.updateAccountBalanceById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, balance);
|
||||||
|
customUserInfoService.lambdaUpdate()
|
||||||
|
.set(PlayCustomUserInfoEntity::getAccumulatedConsumptionAmount, BigDecimal.ZERO)
|
||||||
|
.set(PlayCustomUserInfoEntity::getAccumulatedRechargeAmount, balance)
|
||||||
|
.set(PlayCustomUserInfoEntity::getAccountState, "1")
|
||||||
|
.set(PlayCustomUserInfoEntity::getSubscribeState, "1")
|
||||||
|
.set(PlayCustomUserInfoEntity::getPurchaseState, "1")
|
||||||
|
.set(PlayCustomUserInfoEntity::getMobilePhoneState, "1")
|
||||||
|
.eq(PlayCustomUserInfoEntity::getId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ensureTenantContext() {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String createCouponId(String prefix) {
|
||||||
|
String raw = com.starry.common.utils.IdUtils.getUuid().replace("-", "");
|
||||||
|
String suffix = raw.length() > 10 ? raw.substring(0, 10) : raw;
|
||||||
|
return prefix + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String ensureFixedReductionCoupon(
|
||||||
|
String couponId,
|
||||||
|
OrderConstant.PlaceType placeType,
|
||||||
|
BigDecimal discountAmount,
|
||||||
|
BigDecimal minAmount,
|
||||||
|
String attributionDiscounts) {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCouponInfoEntity existing = couponInfoService.getById(couponId);
|
||||||
|
if (existing != null) {
|
||||||
|
return couponId;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayCouponInfoEntity coupon = new PlayCouponInfoEntity();
|
||||||
|
coupon.setId(couponId);
|
||||||
|
coupon.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
coupon.setCouponName(couponId);
|
||||||
|
coupon.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
|
||||||
|
coupon.setUseMinAmount(minAmount);
|
||||||
|
coupon.setDiscountType(CouponDiscountType.FULL_REDUCTION.getCode());
|
||||||
|
coupon.setDiscountAmount(discountAmount);
|
||||||
|
coupon.setDiscountContent("立减" + discountAmount.setScale(2, RoundingMode.HALF_UP) + "元");
|
||||||
|
coupon.setAttributionDiscounts(attributionDiscounts);
|
||||||
|
coupon.setPlaceType(Collections.singletonList(placeType.getCode()));
|
||||||
|
coupon.setClerkType("0");
|
||||||
|
coupon.setCouponQuantity(200);
|
||||||
|
coupon.setIssuedQuantity(0);
|
||||||
|
coupon.setRemainingQuantity(200);
|
||||||
|
coupon.setClerkObtainedMaxQuantity(5);
|
||||||
|
coupon.setClaimConditionType(CouponClaimConditionType.ALL.code());
|
||||||
|
coupon.setCustomLevelCheckType("0");
|
||||||
|
coupon.setCustomSexCheckType("0");
|
||||||
|
coupon.setCustomLevel(Collections.emptyList());
|
||||||
|
coupon.setCustomSex(Collections.emptyList());
|
||||||
|
coupon.setCustomFollowStatusCheckType("0");
|
||||||
|
coupon.setCustomFollowStatus("1");
|
||||||
|
coupon.setNewUser("0");
|
||||||
|
coupon.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||||
|
couponInfoService.save(coupon);
|
||||||
|
return couponId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String claimCouponForOrder(
|
||||||
|
String couponInfoId,
|
||||||
|
PlaceType placeType,
|
||||||
|
String customerToken) throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get("/wx/coupon/custom/obtainCoupon")
|
||||||
|
.param("id", couponInfoId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(com.starry.common.constant.Constants.CUSTOM_USER_LOGIN_TOKEN,
|
||||||
|
com.starry.common.constant.Constants.TOKEN_PREFIX + customerToken))
|
||||||
|
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk())
|
||||||
|
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.code").value(200))
|
||||||
|
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.data.success").value(true));
|
||||||
|
|
||||||
|
String levelId = placeType == PlaceType.SPECIFIED ? "" : ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID;
|
||||||
|
String clerkId = placeType == PlaceType.SPECIFIED ? ApiTestDataSeeder.DEFAULT_CLERK_ID : "";
|
||||||
|
String queryPayload = "{" +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"levelId\":\"" + levelId + "\"," +
|
||||||
|
"\"clerkId\":\"" + clerkId + "\"," +
|
||||||
|
"\"placeType\":\"" + placeType.getCode() + "\"," +
|
||||||
|
"\"commodityQuantity\":1" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/wx/coupon/custom/queryByOrder")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(com.starry.common.constant.Constants.CUSTOM_USER_LOGIN_TOKEN,
|
||||||
|
com.starry.common.constant.Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(queryPayload))
|
||||||
|
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.status().isOk())
|
||||||
|
.andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
String responseBody = result.getResponse().getContentAsString();
|
||||||
|
JsonNode dataNode = objectMapper.readTree(responseBody).path("data");
|
||||||
|
if (!dataNode.isArray()) {
|
||||||
|
throw new AssertionError("未返回优惠券列表");
|
||||||
|
}
|
||||||
|
String detailId = null;
|
||||||
|
for (JsonNode node : dataNode) {
|
||||||
|
if (!couponInfoId.equals(node.path("couponName").asText())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!"1".equals(node.path("available").asText())) {
|
||||||
|
throw new AssertionError("优惠券不可用: " + node.path("reasonForUnavailableUse").asText());
|
||||||
|
}
|
||||||
|
detailId = node.path("id").asText();
|
||||||
|
if (!StringUtils.hasText(detailId)) {
|
||||||
|
throw new AssertionError("优惠券详情ID缺失");
|
||||||
|
}
|
||||||
|
return detailId;
|
||||||
|
}
|
||||||
|
throw new AssertionError("未在可用列表中找到优惠券 " + couponInfoId + ",响应:" + responseBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void assertCouponUsed(String couponDetailId) {
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
|
||||||
|
if (detail == null) {
|
||||||
|
throw new AssertionError("优惠券详情不存在: " + couponDetailId);
|
||||||
|
}
|
||||||
|
if (!CouponUseState.USED.getCode().equals(detail.getUseState())) {
|
||||||
|
throw new AssertionError("优惠券未标记为已使用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,612 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyList;
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.reset;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.common.task.OverdueOrderHandlerTask;
|
||||||
|
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
|
||||||
|
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.enums.EarningsType;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
|
||||||
|
class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private WxCustomMpService wxCustomMpService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private OverdueOrderHandlerTask overdueOrderHandlerTask;
|
||||||
|
|
||||||
|
@org.springframework.beans.factory.annotation.Autowired
|
||||||
|
private com.starry.admin.modules.order.service.IPlayOrderInfoService orderInfoService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:客户带随机下单请求命中默认等级与服务,接口应返回成功文案,
|
||||||
|
// 并新增一条处于待接单状态的随机订单,金额、商品信息与 remark 与提交参数保持一致。
|
||||||
|
void randomOrderCreatesPendingOrder() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String rawToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, rawToken);
|
||||||
|
|
||||||
|
String remark = "API test random order " + IdUtils.getUuid();
|
||||||
|
ensureTenantContext();
|
||||||
|
long beforeCount = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
String payload = "{" +
|
||||||
|
"\"sex\":\"2\"," +
|
||||||
|
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"commodityQuantity\":1," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"excludeHistory\":\"0\"," +
|
||||||
|
"\"couponIds\":[]," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/random")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + rawToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("下单成功"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
long afterCount = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);
|
||||||
|
|
||||||
|
PlayOrderInfoEntity latest = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
|
||||||
|
Assertions.assertThat(latest).isNotNull();
|
||||||
|
Assertions.assertThat(latest.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
|
||||||
|
Assertions.assertThat(latest.getCommodityId()).isEqualTo(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
|
||||||
|
Assertions.assertThat(latest.getOrderMoney()).isNotNull();
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void randomOrderCancellationReleasesCoupon() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
String remark = "API random cancel coupon " + IdUtils.getUuid();
|
||||||
|
BigDecimal discount = new BigDecimal("15.00");
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
String couponInfoId = createCouponId("cpn-rc-");
|
||||||
|
ensureFixedReductionCoupon(
|
||||||
|
couponInfoId,
|
||||||
|
OrderConstant.PlaceType.RANDOM,
|
||||||
|
discount,
|
||||||
|
new BigDecimal("60.00"),
|
||||||
|
"0");
|
||||||
|
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);
|
||||||
|
|
||||||
|
String payload = "{" +
|
||||||
|
"\"sex\":\"2\"," +
|
||||||
|
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"commodityQuantity\":1," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"excludeHistory\":\"0\"," +
|
||||||
|
"\"couponIds\":[\"" + couponDetailId + "\"]," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/random")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
Assertions.assertThat(order).isNotNull();
|
||||||
|
|
||||||
|
String cancelPayload = "{" +
|
||||||
|
"\"orderId\":\"" + order.getId() + "\"," +
|
||||||
|
"\"refundReason\":\"测试取消\"," +
|
||||||
|
"\"images\":[]" +
|
||||||
|
"}";
|
||||||
|
mockMvc.perform(post("/wx/custom/order/cancellation")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(cancelPayload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("取消成功"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity cancelled = playOrderInfoService.selectOrderInfoById(order.getId());
|
||||||
|
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
|
||||||
|
|
||||||
|
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
|
||||||
|
Assertions.assertThat(detail).isNotNull();
|
||||||
|
Assertions.assertThat(detail.getUseState())
|
||||||
|
.as("取消订单后优惠券应恢复为未使用")
|
||||||
|
.isEqualTo(CouponUseState.UNUSED.getCode());
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:订单已接单后由管理员强制取消,也应释放所使用的优惠券
|
||||||
|
void randomOrderForceCancelReleasesCoupon() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
String remark = "API random force cancel " + IdUtils.getUuid();
|
||||||
|
BigDecimal discount = new BigDecimal("12.00");
|
||||||
|
try {
|
||||||
|
reset(wxCustomMpService);
|
||||||
|
resetCustomerBalance();
|
||||||
|
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||||||
|
|
||||||
|
String couponInfoId = createCouponId("cpn-rf-");
|
||||||
|
ensureFixedReductionCoupon(
|
||||||
|
couponInfoId,
|
||||||
|
OrderConstant.PlaceType.RANDOM,
|
||||||
|
discount,
|
||||||
|
new BigDecimal("60.00"),
|
||||||
|
"0");
|
||||||
|
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);
|
||||||
|
|
||||||
|
String payload = "{" +
|
||||||
|
"\"sex\":\"2\"," +
|
||||||
|
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"commodityQuantity\":1," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"excludeHistory\":\"0\"," +
|
||||||
|
"\"couponIds\":[\"" + couponDetailId + "\"]," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/random")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
Assertions.assertThat(order).isNotNull();
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/clerk/order/accept")
|
||||||
|
.param("id", order.getId())
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("成功"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
orderInfoService.forceCancelOngoingOrder(
|
||||||
|
"2",
|
||||||
|
ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID,
|
||||||
|
order.getId(),
|
||||||
|
order.getFinalAmount(),
|
||||||
|
"管理员强制取消",
|
||||||
|
java.util.Collections.emptyList());
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity cancelled = playOrderInfoService.selectOrderInfoById(order.getId());
|
||||||
|
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
|
||||||
|
|
||||||
|
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
|
||||||
|
Assertions.assertThat(detail).isNotNull();
|
||||||
|
Assertions.assertThat(detail.getUseState())
|
||||||
|
.as("强制取消订单后优惠券应恢复为未使用")
|
||||||
|
.isEqualTo(CouponUseState.UNUSED.getCode());
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:随机单携带店铺承担的满减券下单,需依据折后金额推送通知,
|
||||||
|
// 完整履约后优惠券应置为已使用且收益与预计工资保持一致。
|
||||||
|
void randomOrderLifecycleWithCouponAdjustsRevenueAndNotifications() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
String remark = "API random coupon " + IdUtils.getUuid();
|
||||||
|
BigDecimal discount = new BigDecimal("20.00");
|
||||||
|
try {
|
||||||
|
reset(wxCustomMpService);
|
||||||
|
resetCustomerBalance();
|
||||||
|
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||||||
|
|
||||||
|
BigDecimal grossAmount = ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE;
|
||||||
|
String couponInfoId = createCouponId("cpn-r-");
|
||||||
|
ensureFixedReductionCoupon(
|
||||||
|
couponInfoId,
|
||||||
|
OrderConstant.PlaceType.RANDOM,
|
||||||
|
discount,
|
||||||
|
new BigDecimal("60.00"),
|
||||||
|
"0");
|
||||||
|
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);
|
||||||
|
|
||||||
|
String payload = "{" +
|
||||||
|
"\"sex\":\"2\"," +
|
||||||
|
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"commodityQuantity\":1," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"excludeHistory\":\"0\"," +
|
||||||
|
"\"couponIds\":[\"" + couponDetailId + "\"]," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/random")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("下单成功"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
|
||||||
|
Assertions.assertThat(order).isNotNull();
|
||||||
|
Assertions.assertThat(order.getCouponIds()).contains(couponDetailId);
|
||||||
|
|
||||||
|
BigDecimal expectedNet = grossAmount.subtract(discount).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(expectedNet);
|
||||||
|
Assertions.assertThat(order.getDiscountAmount()).isEqualByComparingTo(discount);
|
||||||
|
|
||||||
|
verify(wxCustomMpService).sendCreateOrderMessageBatch(
|
||||||
|
anyList(),
|
||||||
|
eq(order.getOrderNo()),
|
||||||
|
eq(expectedNet.toString()),
|
||||||
|
eq(order.getCommodityName()),
|
||||||
|
eq(order.getId()));
|
||||||
|
|
||||||
|
String orderId = order.getId();
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/clerk/order/accept")
|
||||||
|
.param("id", orderId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("成功"));
|
||||||
|
|
||||||
|
verify(wxCustomMpService).sendOrderMessageAsync(argThat(o -> orderId.equals(o.getId())));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/clerk/order/start")
|
||||||
|
.param("id", orderId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("成功"));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/custom/order/end")
|
||||||
|
.param("id", orderId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("成功"));
|
||||||
|
|
||||||
|
verify(wxCustomMpService).sendOrderFinishMessageAsync(argThat(o -> orderId.equals(o.getId())));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity completedOrder = playOrderInfoService.selectOrderInfoById(orderId);
|
||||||
|
int ratio = completedOrder.getEstimatedRevenueRatio();
|
||||||
|
BigDecimal baseRevenue = grossAmount
|
||||||
|
.multiply(BigDecimal.valueOf(ratio))
|
||||||
|
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||||
|
PlayCouponDetailsReturnVo detail = couponDetailsService.selectPlayCouponDetailsById(couponDetailId);
|
||||||
|
BigDecimal clerkDiscount = BigDecimal.ZERO;
|
||||||
|
if (detail != null && "0".equals(detail.getAttributionDiscounts())) {
|
||||||
|
BigDecimal discountAmount = detail.getDiscountAmount() == null ? BigDecimal.ZERO : detail.getDiscountAmount();
|
||||||
|
clerkDiscount = discountAmount;
|
||||||
|
}
|
||||||
|
BigDecimal expectedRevenue = baseRevenue.subtract(clerkDiscount).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
Assertions.assertThat(completedOrder.getEstimatedRevenue()).isEqualByComparingTo(expectedRevenue);
|
||||||
|
|
||||||
|
EarningsLineEntity earningsLine = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
Assertions.assertThat(earningsLine).isNotNull();
|
||||||
|
Assertions.assertThat(earningsLine.getAmount()).isEqualByComparingTo(expectedRevenue);
|
||||||
|
|
||||||
|
assertCouponUsed(couponDetailId);
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:模拟随机订单完整生命周期——客户下单、陪玩师接单/开局、客户完结,
|
||||||
|
// 期间验证微信通知被触发、收益记录生成、冻结解冻时间正确,并校准日终统计接口返回的订单数、GMV 与预计收益。
|
||||||
|
void randomOrderLifecycleGeneratesEarningsAndNotifications() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
String remark = "API random lifecycle " + IdUtils.getUuid();
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||||||
|
|
||||||
|
LocalDateTime overviewWindowStart = LocalDateTime.now().minusMinutes(5);
|
||||||
|
LocalDateTime overviewWindowEnd = LocalDateTime.now().plusMinutes(5);
|
||||||
|
OverviewSnapshot overviewBefore = fetchOverview(overviewWindowStart, overviewWindowEnd);
|
||||||
|
|
||||||
|
String orderId = placeRandomOrder(remark, customerToken);
|
||||||
|
|
||||||
|
reset(wxCustomMpService);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
mockMvc.perform(get("/wx/clerk/order/accept")
|
||||||
|
.param("id", orderId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("成功"));
|
||||||
|
|
||||||
|
verify(wxCustomMpService).sendOrderMessageAsync(argThat(order -> order.getId().equals(orderId)));
|
||||||
|
|
||||||
|
reset(wxCustomMpService);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
mockMvc.perform(get("/wx/clerk/order/start")
|
||||||
|
.param("id", orderId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("成功"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
long earningsBefore = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||||
|
.count();
|
||||||
|
Assertions.assertThat(earningsBefore).isZero();
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
mockMvc.perform(get("/wx/custom/order/end")
|
||||||
|
.param("id", orderId)
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("成功"));
|
||||||
|
|
||||||
|
verify(wxCustomMpService).sendOrderFinishMessageAsync(argThat(order -> order.getId().equals(orderId)));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
long earningsAfter = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||||
|
.count();
|
||||||
|
Assertions.assertThat(earningsAfter).isEqualTo(1);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity completedOrder = playOrderInfoService.selectOrderInfoById(orderId);
|
||||||
|
Assertions.assertThat(completedOrder.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.COMPLETED.getCode());
|
||||||
|
Assertions.assertThat(completedOrder.getEstimatedRevenue()).isNotNull();
|
||||||
|
|
||||||
|
EarningsLineEntity earningsLine = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, orderId)
|
||||||
|
.one();
|
||||||
|
Assertions.assertThat(earningsLine).isNotNull();
|
||||||
|
Assertions.assertThat(earningsLine.getAmount()).isEqualByComparingTo(completedOrder.getEstimatedRevenue());
|
||||||
|
int freezeHours = freezePolicyService.resolveFreezeHours(
|
||||||
|
ApiTestDataSeeder.DEFAULT_TENANT_ID,
|
||||||
|
ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
LocalDateTime expectedUnlock = completedOrder.getOrderEndTime().plusHours(freezeHours);
|
||||||
|
Assertions.assertThat(earningsLine.getUnlockTime()).isEqualTo(expectedUnlock);
|
||||||
|
Assertions.assertThat(earningsLine.getUnlockTime()).isAfter(LocalDateTime.now().minusMinutes(5));
|
||||||
|
Assertions.assertThat(earningsLine.getEarningType()).isEqualTo(EarningsType.ORDER);
|
||||||
|
|
||||||
|
OverviewSnapshot overviewAfter = fetchOverview(overviewWindowStart, overviewWindowEnd);
|
||||||
|
Assertions.assertThat(overviewAfter.totalOrderCount - overviewBefore.totalOrderCount)
|
||||||
|
.isEqualTo(1);
|
||||||
|
Assertions.assertThat(overviewAfter.totalGmv.subtract(overviewBefore.totalGmv).setScale(2, RoundingMode.HALF_UP))
|
||||||
|
.isEqualByComparingTo(completedOrder.getFinalAmount());
|
||||||
|
Assertions.assertThat(overviewAfter.totalEstimatedRevenue.subtract(overviewBefore.totalEstimatedRevenue).setScale(2, RoundingMode.HALF_UP))
|
||||||
|
.isEqualByComparingTo(completedOrder.getEstimatedRevenue());
|
||||||
|
Assertions.assertThat(overviewAfter.clerkOrderCount - overviewBefore.clerkOrderCount)
|
||||||
|
.isEqualTo(1);
|
||||||
|
Assertions.assertThat(overviewAfter.clerkGmv.subtract(overviewBefore.clerkGmv).setScale(2, RoundingMode.HALF_UP))
|
||||||
|
.isEqualByComparingTo(completedOrder.getFinalAmount());
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OverviewSnapshot fetchOverview(LocalDateTime start, LocalDateTime end) throws Exception {
|
||||||
|
String payload = "{" +
|
||||||
|
"\"includeSummary\":true," +
|
||||||
|
"\"includeRankings\":true," +
|
||||||
|
"\"limit\":5," +
|
||||||
|
"\"endOrderTime\":[\"" + start.format(DATE_TIME_FORMATTER) + "\",\""
|
||||||
|
+ end.format(DATE_TIME_FORMATTER) + "\"]" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(post("/statistics/performance/overview")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).path("data");
|
||||||
|
OverviewSnapshot snapshot = new OverviewSnapshot();
|
||||||
|
JsonNode summary = data.path("summary");
|
||||||
|
snapshot.totalOrderCount = summary.path("totalOrderCount").asInt();
|
||||||
|
snapshot.totalGmv = new BigDecimal(summary.path("totalGmv").asText("0"));
|
||||||
|
snapshot.totalEstimatedRevenue = new BigDecimal(summary.path("totalEstimatedRevenue").asText("0"));
|
||||||
|
|
||||||
|
JsonNode rankings = data.path("rankings");
|
||||||
|
if (rankings.isArray()) {
|
||||||
|
for (JsonNode node : rankings) {
|
||||||
|
if (ApiTestDataSeeder.DEFAULT_CLERK_ID.equals(node.path("clerkId").asText())) {
|
||||||
|
snapshot.clerkOrderCount = node.path("orderCount").asInt();
|
||||||
|
snapshot.clerkGmv = new BigDecimal(node.path("gmv").asText("0"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class OverviewSnapshot {
|
||||||
|
private int totalOrderCount;
|
||||||
|
private BigDecimal totalGmv = BigDecimal.ZERO;
|
||||||
|
private BigDecimal totalEstimatedRevenue = BigDecimal.ZERO;
|
||||||
|
private int clerkOrderCount;
|
||||||
|
private BigDecimal clerkGmv = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String placeRandomOrder(String remark, String customerToken) throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
long beforeCount = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
String payload = "{" +
|
||||||
|
"\"sex\":\"2\"," +
|
||||||
|
"\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"commodityQuantity\":1," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"excludeHistory\":\"0\"," +
|
||||||
|
"\"couponIds\":[]," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/random")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("下单成功"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
|
||||||
|
Assertions.assertThat(order).isNotNull();
|
||||||
|
Assertions.assertThat(order.getPlaceType()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
long afterCount = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.count();
|
||||||
|
Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);
|
||||||
|
|
||||||
|
verify(wxCustomMpService).sendCreateOrderMessageBatch(
|
||||||
|
anyList(),
|
||||||
|
eq(order.getOrderNo()),
|
||||||
|
eq(order.getFinalAmount().toString()),
|
||||||
|
eq(order.getCommodityName()),
|
||||||
|
eq(order.getId()));
|
||||||
|
verify(overdueOrderHandlerTask).enqueue(order.getId() + "_" + ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
|
||||||
|
reset(wxCustomMpService);
|
||||||
|
reset(overdueOrderHandlerTask);
|
||||||
|
|
||||||
|
return order.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
|
class WxCustomRewardOrderApiTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:客户指定打赏金额下单时,应即时扣减账户余额、生成已完成的打赏订单并同步收益记录,
|
||||||
|
// 同时校验订单归属陪玩师正确且金额与输入一致,确保余额打赏流程闭环。
|
||||||
|
void rewardOrderConsumesBalanceAndGeneratesEarnings() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
BigDecimal initialBalance = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID).getAccountBalance();
|
||||||
|
|
||||||
|
String remark = "API reward order " + IdUtils.getUuid();
|
||||||
|
BigDecimal rewardAmount = new BigDecimal("18.75");
|
||||||
|
|
||||||
|
String payload = "{" +
|
||||||
|
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
|
||||||
|
"\"money\":\"" + rewardAmount + "\"," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/reward")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("成功"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
|
||||||
|
Assertions.assertThat(order).isNotNull();
|
||||||
|
Assertions.assertThat(order.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.COMPLETED.getCode());
|
||||||
|
Assertions.assertThat(order.getPlaceType()).isEqualTo(OrderConstant.PlaceType.REWARD.getCode());
|
||||||
|
Assertions.assertThat(order.getRewardType()).isEqualTo(OrderConstant.RewardType.BALANCE.getCode());
|
||||||
|
Assertions.assertThat(order.getAcceptBy()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(rewardAmount);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
long earningsCount = earningsService.lambdaQuery()
|
||||||
|
.eq(EarningsLineEntity::getOrderId, order.getId())
|
||||||
|
.count();
|
||||||
|
Assertions.assertThat(earningsCount).isEqualTo(1);
|
||||||
|
|
||||||
|
BigDecimal expectedBalance = initialBalance.subtract(rewardAmount);
|
||||||
|
Assertions.assertThat(customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID).getAccountBalance())
|
||||||
|
.isEqualByComparingTo(expectedBalance);
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
package com.starry.admin.api;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.reset;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.starry.admin.common.apitest.ApiTestDataSeeder;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
|
||||||
|
import com.starry.admin.modules.weichat.service.WxCustomMpService;
|
||||||
|
import com.starry.admin.utils.SecurityUtils;
|
||||||
|
import com.starry.common.constant.Constants;
|
||||||
|
import com.starry.common.context.CustomSecurityContextHolder;
|
||||||
|
import com.starry.common.utils.IdUtils;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.Collections;
|
||||||
|
import org.assertj.core.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
|
||||||
|
class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private WxCustomMpService wxCustomMpService;
|
||||||
|
|
||||||
|
@org.springframework.beans.factory.annotation.Autowired
|
||||||
|
private com.starry.admin.modules.order.service.IPlayOrderInfoService orderInfoService;
|
||||||
|
|
||||||
|
@org.springframework.beans.factory.annotation.Autowired
|
||||||
|
private com.starry.admin.modules.weichat.service.WxTokenService clerkWxTokenService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:指定单取消后优惠券应恢复为未使用
|
||||||
|
void specifiedOrderCancellationReleasesCoupon() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
String remark = "API specified cancel " + IdUtils.getUuid();
|
||||||
|
BigDecimal discount = new BigDecimal("10.00");
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
String couponInfoId = createCouponId("cpn-sc-");
|
||||||
|
ensureFixedReductionCoupon(
|
||||||
|
couponInfoId,
|
||||||
|
OrderConstant.PlaceType.SPECIFIED,
|
||||||
|
discount,
|
||||||
|
new BigDecimal("60.00"),
|
||||||
|
"0");
|
||||||
|
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.SPECIFIED, customerToken);
|
||||||
|
|
||||||
|
String payload = "{" +
|
||||||
|
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"commodityQuantity\":1," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"couponIds\":[\"" + couponDetailId + "\"]," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/commodity")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
Assertions.assertThat(order).isNotNull();
|
||||||
|
|
||||||
|
String cancelPayload = "{" +
|
||||||
|
"\"orderId\":\"" + order.getId() + "\"," +
|
||||||
|
"\"refundReason\":\"测试取消\"," +
|
||||||
|
"\"images\":[]" +
|
||||||
|
"}";
|
||||||
|
mockMvc.perform(post("/wx/custom/order/cancellation")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(cancelPayload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("取消成功"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity cancelled = playOrderInfoService.selectOrderInfoById(order.getId());
|
||||||
|
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
|
||||||
|
|
||||||
|
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
|
||||||
|
Assertions.assertThat(detail.getUseState())
|
||||||
|
.as("取消指定单后优惠券应恢复为未使用")
|
||||||
|
.isEqualTo(CouponUseState.UNUSED.getCode());
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:指定单使用满减券下单后,应按折后金额推送创建通知,
|
||||||
|
// 并将预计收益扣除优惠金额,同时把优惠券详情标记为已使用。
|
||||||
|
void specifiedOrderWithCouponAdjustsAmountAndRevenue() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
String remark = "API specified coupon " + IdUtils.getUuid();
|
||||||
|
BigDecimal discount = new BigDecimal("15.00");
|
||||||
|
try {
|
||||||
|
reset(wxCustomMpService);
|
||||||
|
resetCustomerBalance();
|
||||||
|
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
BigDecimal grossAmount = ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE;
|
||||||
|
String couponInfoId = createCouponId("cpn-s-");
|
||||||
|
ensureFixedReductionCoupon(
|
||||||
|
couponInfoId,
|
||||||
|
OrderConstant.PlaceType.SPECIFIED,
|
||||||
|
discount,
|
||||||
|
new BigDecimal("60.00"),
|
||||||
|
"0");
|
||||||
|
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.SPECIFIED, customerToken);
|
||||||
|
|
||||||
|
String payload = "{" +
|
||||||
|
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"commodityQuantity\":1," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"couponIds\":[\"" + couponDetailId + "\"]," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/commodity")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("成功"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
|
||||||
|
Assertions.assertThat(order).isNotNull();
|
||||||
|
Assertions.assertThat(order.getCouponIds()).contains(couponDetailId);
|
||||||
|
|
||||||
|
BigDecimal expectedNet = grossAmount.subtract(discount).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(expectedNet);
|
||||||
|
Assertions.assertThat(order.getDiscountAmount()).isEqualByComparingTo(discount);
|
||||||
|
|
||||||
|
verify(wxCustomMpService).sendCreateOrderMessage(
|
||||||
|
eq(ApiTestDataSeeder.DEFAULT_TENANT_ID),
|
||||||
|
eq(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID),
|
||||||
|
anyString(),
|
||||||
|
eq(expectedNet.toString()),
|
||||||
|
eq(order.getCommodityName()),
|
||||||
|
eq(order.getId()));
|
||||||
|
|
||||||
|
int ratio = order.getEstimatedRevenueRatio();
|
||||||
|
BigDecimal baseRevenue = grossAmount
|
||||||
|
.multiply(BigDecimal.valueOf(ratio))
|
||||||
|
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||||
|
PlayCouponDetailsReturnVo detail = couponDetailsService.selectPlayCouponDetailsById(couponDetailId);
|
||||||
|
BigDecimal clerkDiscount = BigDecimal.ZERO;
|
||||||
|
if (detail != null && "0".equals(detail.getAttributionDiscounts())) {
|
||||||
|
BigDecimal discountAmount = detail.getDiscountAmount() == null ? BigDecimal.ZERO : detail.getDiscountAmount();
|
||||||
|
clerkDiscount = discountAmount;
|
||||||
|
}
|
||||||
|
BigDecimal expectedRevenue = baseRevenue.subtract(clerkDiscount).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
Assertions.assertThat(order.getEstimatedRevenue()).isEqualByComparingTo(expectedRevenue);
|
||||||
|
|
||||||
|
assertCouponUsed(couponDetailId);
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:指定单在接单后由管理员强制取消时,优惠券应恢复为未使用
|
||||||
|
void specifiedOrderForceCancelReleasesCoupon() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
String remark = "API specified force cancel " + IdUtils.getUuid();
|
||||||
|
BigDecimal discount = new BigDecimal("8.00");
|
||||||
|
try {
|
||||||
|
reset(wxCustomMpService);
|
||||||
|
resetCustomerBalance();
|
||||||
|
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);
|
||||||
|
|
||||||
|
String couponInfoId = createCouponId("cpn-sf-");
|
||||||
|
ensureFixedReductionCoupon(
|
||||||
|
couponInfoId,
|
||||||
|
OrderConstant.PlaceType.SPECIFIED,
|
||||||
|
discount,
|
||||||
|
new BigDecimal("60.00"),
|
||||||
|
"0");
|
||||||
|
String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.SPECIFIED, customerToken);
|
||||||
|
|
||||||
|
String payload = "{" +
|
||||||
|
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"commodityQuantity\":1," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"couponIds\":[\"" + couponDetailId + "\"]," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/commodity")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
Assertions.assertThat(order).isNotNull();
|
||||||
|
|
||||||
|
mockMvc.perform(get("/wx/clerk/order/accept")
|
||||||
|
.param("id", order.getId())
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("成功"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
orderInfoService.forceCancelOngoingOrder(
|
||||||
|
"2",
|
||||||
|
ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID,
|
||||||
|
order.getId(),
|
||||||
|
order.getFinalAmount(),
|
||||||
|
"管理员强制取消",
|
||||||
|
java.util.Collections.emptyList());
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity cancelled = playOrderInfoService.selectOrderInfoById(order.getId());
|
||||||
|
Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());
|
||||||
|
|
||||||
|
PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
|
||||||
|
Assertions.assertThat(detail.getUseState())
|
||||||
|
.as("强制取消指定单后优惠券应恢复为未使用")
|
||||||
|
.isEqualTo(CouponUseState.UNUSED.getCode());
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 测试用例:客户携带指定陪玩师和服务下单时,接口需返回成功,并生成待支付状态的指定订单,
|
||||||
|
// 验证订单金额与种子服务价格一致、陪玩师被正确指派,同时触发微信创建订单通知。
|
||||||
|
void specifiedOrderCreatesPendingOrder() throws Exception {
|
||||||
|
SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
try {
|
||||||
|
resetCustomerBalance();
|
||||||
|
String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
|
||||||
|
customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
|
||||||
|
|
||||||
|
String remark = "API specified order " + IdUtils.getUuid();
|
||||||
|
ensureTenantContext();
|
||||||
|
long beforeCount = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.SPECIFIED.getCode())
|
||||||
|
.count();
|
||||||
|
|
||||||
|
String payload = "{" +
|
||||||
|
"\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
|
||||||
|
"\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
|
||||||
|
"\"commodityQuantity\":1," +
|
||||||
|
"\"weiChatCode\":\"apitest-customer-wx\"," +
|
||||||
|
"\"couponIds\":[]," +
|
||||||
|
"\"remark\":\"" + remark + "\"" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
reset(wxCustomMpService);
|
||||||
|
|
||||||
|
mockMvc.perform(post("/wx/custom/order/commodity")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(payload))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data").value("成功"));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getRemark, remark)
|
||||||
|
.orderByDesc(PlayOrderInfoEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
|
||||||
|
Assertions.assertThat(order).isNotNull();
|
||||||
|
Assertions.assertThat(order.getPlaceType()).isEqualTo(OrderConstant.PlaceType.SPECIFIED.getCode());
|
||||||
|
Assertions.assertThat(order.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
|
||||||
|
Assertions.assertThat(order.getAcceptBy()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE);
|
||||||
|
|
||||||
|
verify(wxCustomMpService).sendCreateOrderMessage(
|
||||||
|
eq(ApiTestDataSeeder.DEFAULT_TENANT_ID),
|
||||||
|
eq(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID),
|
||||||
|
anyString(),
|
||||||
|
eq(order.getFinalAmount().toString()),
|
||||||
|
eq(order.getCommodityName()),
|
||||||
|
eq(order.getId()));
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
long afterCount = playOrderInfoService.lambdaQuery()
|
||||||
|
.eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
.eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.SPECIFIED.getCode())
|
||||||
|
.count();
|
||||||
|
Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);
|
||||||
|
} finally {
|
||||||
|
CustomSecurityContextHolder.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
pom.xml
10
pom.xml
@@ -333,6 +333,14 @@
|
|||||||
</build>
|
</build>
|
||||||
|
|
||||||
<profiles>
|
<profiles>
|
||||||
|
<profile>
|
||||||
|
<id>apitest</id>
|
||||||
|
<properties>
|
||||||
|
<spring.profiles.active>apitest</spring.profiles.active>
|
||||||
|
<apitest.mysql.port>33306</apitest.mysql.port>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
|
||||||
<!-- macOS Apple Silicon -->
|
<!-- macOS Apple Silicon -->
|
||||||
<profile>
|
<profile>
|
||||||
<id>osx-arm64</id>
|
<id>osx-arm64</id>
|
||||||
@@ -369,4 +377,4 @@
|
|||||||
</dependencies>
|
</dependencies>
|
||||||
</profile>
|
</profile>
|
||||||
</profiles>
|
</profiles>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
Reference in New Issue
Block a user