From 6a3b4fef1f00685906e47d848678cf1025394338 Mon Sep 17 00:00:00 2001 From: irving Date: Sat, 17 Jan 2026 00:49:54 -0500 Subject: [PATCH] test(apitest): add e2e seed endpoints and coverage --- .../common/apitest/ApiTestDataSeeder.java | 118 ++- .../weichat/controller/WxOauthController.java | 952 +++++++++++++++++- .../weichat/service/WxCustomMpService.java | 6 + .../main/resources/application-apitest.yml | 11 +- .../api/WxOauthAdminTestAuthApiTest.java | 52 + .../admin/api/WxOauthE2eSeedOrderApiTest.java | 51 + .../WxOauthE2eSeedWageAdjustmentApiTest.java | 48 + 7 files changed, 1228 insertions(+), 10 deletions(-) create mode 100644 play-admin/src/test/java/com/starry/admin/api/WxOauthAdminTestAuthApiTest.java create mode 100644 play-admin/src/test/java/com/starry/admin/api/WxOauthE2eSeedOrderApiTest.java create mode 100644 play-admin/src/test/java/com/starry/admin/api/WxOauthE2eSeedWageAdjustmentApiTest.java diff --git a/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java b/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java index 430e166..2983877 100644 --- a/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java +++ b/play-admin/src/main/java/com/starry/admin/common/apitest/ApiTestDataSeeder.java @@ -28,7 +28,9 @@ import com.starry.admin.modules.shop.service.IPlayClerkGiftInfoService; 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.mapper.SysMenuMapper; import com.starry.admin.modules.system.mapper.SysUserMapper; +import com.starry.admin.modules.system.module.entity.SysMenuEntity; 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; @@ -37,12 +39,14 @@ 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.constant.UserConstants; 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 java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.CommandLineRunner; @@ -77,6 +81,7 @@ public class ApiTestDataSeeder implements CommandLineRunner { 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"); + public static final BigDecimal E2E_CUSTOMER_BALANCE = new BigDecimal("1000.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"); @@ -86,6 +91,7 @@ public class ApiTestDataSeeder implements CommandLineRunner { private final ISysTenantService tenantService; private final SysUserService sysUserService; private final SysUserMapper sysUserMapper; + private final SysMenuMapper sysMenuMapper; private final IPlayPersonnelGroupInfoService personnelGroupInfoService; private final IPlayClerkLevelInfoService clerkLevelInfoService; private final IPlayClerkUserInfoService clerkUserInfoService; @@ -108,6 +114,7 @@ public class ApiTestDataSeeder implements CommandLineRunner { ISysTenantService tenantService, SysUserService sysUserService, SysUserMapper sysUserMapper, + SysMenuMapper sysMenuMapper, IPlayPersonnelGroupInfoService personnelGroupInfoService, IPlayClerkLevelInfoService clerkLevelInfoService, IPlayClerkUserInfoService clerkUserInfoService, @@ -128,6 +135,7 @@ public class ApiTestDataSeeder implements CommandLineRunner { this.tenantService = tenantService; this.sysUserService = sysUserService; this.sysUserMapper = sysUserMapper; + this.sysMenuMapper = sysMenuMapper; this.personnelGroupInfoService = personnelGroupInfoService; this.clerkLevelInfoService = clerkLevelInfoService; this.clerkUserInfoService = clerkUserInfoService; @@ -149,6 +157,7 @@ public class ApiTestDataSeeder implements CommandLineRunner { @Override @Transactional public void run(String... args) { + seedPcTenantWagesMenu(); seedTenantPackage(); seedTenant(); @@ -173,6 +182,98 @@ public class ApiTestDataSeeder implements CommandLineRunner { } } + private void seedPcTenantWagesMenu() { + // Minimal menu tree for pc-tenant E2E: /play/clerk/wages -> play/clerk/wages/index.vue + // This is apitest-only; prod/dev menus are managed by ops/admin tooling. + SysMenuEntity playRoot = ensureMenu( + "陪聊管理", + "PlayManage", + 0L, + UserConstants.TYPE_DIR, + "/play", + UserConstants.LAYOUT, + 50); + + SysMenuEntity clerkDir = ensureMenu( + "店员管理", + "ClerkManage", + playRoot.getMenuId(), + UserConstants.TYPE_DIR, + "clerk", + "", + 1); + + ensureMenu( + "收益管理", + "ClerkWages", + clerkDir.getMenuId(), + UserConstants.TYPE_MENU, + "wages", + "play/clerk/wages/index", + 1); + } + + private SysMenuEntity ensureMenu( + String menuName, + String menuCode, + Long parentId, + String menuType, + String path, + String component, + Integer sort) { + Optional existing = sysMenuMapper.selectList(Wrappers.lambdaQuery() + .eq(SysMenuEntity::getDeleted, false) + .eq(SysMenuEntity::getParentId, parentId) + .eq(SysMenuEntity::getMenuCode, menuCode) + .last("limit 1")) + .stream() + .findFirst(); + if (existing.isPresent()) { + SysMenuEntity current = existing.get(); + boolean changed = false; + if (!Objects.equals(current.getPath(), path)) { + current.setPath(path); + changed = true; + } + if (!Objects.equals(current.getComponent(), component)) { + current.setComponent(component); + changed = true; + } + if (changed) { + current.setUpdatedBy("apitest-seed"); + current.setUpdatedTime(new Date()); + sysMenuMapper.updateById(current); + log.info("Updated apitest sys_menu '{}' path='{}' component='{}'", menuName, path, component); + } + return current; + } + + SysMenuEntity entity = new SysMenuEntity(); + entity.setMenuName(menuName); + entity.setMenuCode(menuCode); + entity.setIcon("el-icon-menu"); + entity.setPermission(""); + entity.setMenuLevel(parentId == 0 ? 1L : 2L); + entity.setSort(sort); + entity.setParentId(parentId); + entity.setMenuType(menuType); + entity.setStatus(0); + entity.setRemark(menuName); + entity.setPath(path); + entity.setComponent(component); + entity.setRouterQuery(""); + entity.setIsFrame(0); + entity.setVisible(1); + entity.setDeleted(Boolean.FALSE); + entity.setCreatedBy("apitest-seed"); + entity.setCreatedTime(new Date()); + entity.setUpdatedBy("apitest-seed"); + entity.setUpdatedTime(new Date()); + sysMenuMapper.insert(entity); + log.info("Inserted apitest sys_menu '{}' path='{}' parentId={}", menuName, path, parentId); + return entity; + } + private void seedTenantPackage() { long existing = tenantPackageService.count(Wrappers.lambdaQuery() .eq(SysTenantPackageEntity::getPackageId, DEFAULT_PACKAGE_ID)); @@ -496,22 +597,27 @@ public class ApiTestDataSeeder implements CommandLineRunner { 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(); } + if (mapping != null) { + clerkCommodityService.update(Wrappers.lambdaUpdate() + .eq(PlayClerkCommodityEntity::getId, DEFAULT_CLERK_COMMODITY_ID) + .set(PlayClerkCommodityEntity::getCommodityId, DEFAULT_COMMODITY_PARENT_ID) + .set(PlayClerkCommodityEntity::getCommodityName, commodityName) + .set(PlayClerkCommodityEntity::getEnablingState, "1")); + log.info("API test clerk commodity {} already exists, state refreshed", DEFAULT_CLERK_COMMODITY_ID); + return; + } + PlayClerkCommodityEntity entity = new PlayClerkCommodityEntity(); entity.setId(DEFAULT_CLERK_COMMODITY_ID); entity.setTenantId(DEFAULT_TENANT_ID); entity.setClerkId(DEFAULT_CLERK_ID); - entity.setCommodityId(DEFAULT_COMMODITY_ID); + entity.setCommodityId(DEFAULT_COMMODITY_PARENT_ID); entity.setCommodityName(commodityName); entity.setEnablingState("1"); entity.setSort(1); diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxOauthController.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxOauthController.java index 9e568a0..eb42d69 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxOauthController.java +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/controller/WxOauthController.java @@ -5,30 +5,57 @@ import static com.starry.common.constant.Constants.*; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson2.JSONObject; +import com.starry.admin.common.apitest.ApiTestDataSeeder; import com.starry.admin.common.aspect.ClerkUserLogin; import com.starry.admin.common.aspect.CustomUserLogin; import com.starry.admin.common.conf.ThreadLocalRequestDetail; import com.starry.admin.common.exception.CustomException; +import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity; import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity; +import com.starry.admin.modules.clerk.module.enums.ClerkRoleStatus; +import com.starry.admin.modules.clerk.module.enums.ListingStatus; +import com.starry.admin.modules.clerk.module.enums.OnboardingStatus; +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.entity.PlayOrderInfoEntity; +import com.starry.admin.modules.order.service.IPlayOrderInfoService; +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.service.IPlayCommodityAndLevelInfoService; +import com.starry.admin.modules.system.module.entity.SysTenantEntity; +import com.starry.admin.modules.system.module.entity.SysUserEntity; +import com.starry.admin.modules.system.service.ISysTenantService; +import com.starry.admin.modules.system.service.LoginService; +import com.starry.admin.modules.system.service.SysUserService; +import com.starry.admin.modules.system.vo.LoginVo; import com.starry.admin.modules.weichat.entity.WxUserLoginVo; import com.starry.admin.modules.weichat.entity.WxUserQueryAddressVo; import com.starry.admin.modules.weichat.service.WxCustomMpService; import com.starry.admin.modules.weichat.service.WxOauthService; import com.starry.admin.modules.weichat.service.WxTokenService; +import com.starry.admin.modules.withdraw.entity.EarningsLineEntity; +import com.starry.admin.modules.withdraw.enums.EarningsSourceType; +import com.starry.admin.modules.withdraw.enums.EarningsType; +import com.starry.admin.modules.withdraw.service.IEarningsService; import com.starry.admin.utils.SecurityUtils; +import com.starry.admin.utils.TenantScope; import com.starry.common.redis.RedisCache; import com.starry.common.result.R; import com.starry.common.result.ResultCodeEnum; +import com.starry.common.utils.IdUtils; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -38,6 +65,7 @@ import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.error.WxErrorException; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.StringUtils; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -66,6 +94,28 @@ public class WxOauthController { private WxOauthService wxOauthService; @Resource private RedisCache redisCache; + @Resource + private LoginService loginService; + @Resource + private com.starry.admin.common.component.JwtToken jwtToken; + + @Resource + private ISysTenantService tenantService; + @Resource + private SysUserService sysUserService; + @Resource + private PasswordEncoder passwordEncoder; + @Resource + private IPlayCommodityAndLevelInfoService commodityAndLevelInfoService; + @Resource + private IPlayPersonnelGroupInfoService personnelGroupInfoService; + @Resource + private IPlayClerkLevelInfoService clerkLevelInfoService; + + @Resource + private IPlayOrderInfoService orderInfoService; + @Resource + private IEarningsService earningsService; @Resource private Environment environment; @@ -99,6 +149,899 @@ public class WxOauthController { && testAuthSecret.equals(headerValue); } + @ApiOperation(value = "测试用:后台管理员登录", notes = "dev/test/apitest 专用;返回与 /login 相同的 tokenHead/token") + @PostMapping("/admin/loginByUsername") + public R adminLoginByUsername( + @RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth, + @ApiParam(value = "登录信息", required = true) @Validated @RequestBody LoginVo loginVo) { + if (!isTestAuthHeaderValid(testAuth)) { + return rejectTestAuth(); + } + if (!StringUtils.hasText(loginVo.getUserName()) + || !StringUtils.hasText(loginVo.getPassWord()) + || !StringUtils.hasText(loginVo.getTenantKey())) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "missing required fields"); + } + return R.ok(jwtToken.createToken(loginService.newLogin( + loginVo.getUserName(), + loginVo.getPassWord(), + loginVo.getTenantKey()))); + } + + @ApiOperation(value = "测试用:种子数据(工资调整)", notes = "创建 1 个店员 + 2 个订单收益 line(amount>0, 有 order_id),用于 E2E 测试") + @PostMapping("/e2e/seed/wage-adjustment") + public R seedWageAdjustment( + @RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth) { + if (!isTestAuthHeaderValid(testAuth)) { + return rejectTestAuth(); + } + + // Default to apitest tenant when available. + final String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID; + final String tenantKey = ApiTestDataSeeder.DEFAULT_TENANT_KEY; + final String adminUserName = ApiTestDataSeeder.DEFAULT_ADMIN_USERNAME; + final String adminPassword = "apitest-secret"; + final String clerkId = ApiTestDataSeeder.DEFAULT_CLERK_ID; + final String customerId = ApiTestDataSeeder.DEFAULT_CUSTOMER_ID; + + try (TenantScope ignored = TenantScope.use(tenantId)) { + PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(clerkId); + if (clerk == null) { + // ApiTestDataSeeder runs in a single transaction during startup; the data may not be committed yet. + // This endpoint must never insert fixed ids, otherwise it can race with seeder and crash the app. + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "seed not ready"); + } + PlayCustomUserInfoEntity customer = customUserInfoService.getById(customerId); + if (customer == null) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "seed not ready"); + } + + LocalDateTime now = LocalDateTime.now().withNano(0); + LocalDateTime begin = now.minusDays(7); + LocalDateTime end = now.plusSeconds(1); + + // Make baseAmount deterministic: 100 + 50 = 150 + String order1 = seedOrder(tenantId, clerkId, now.minusDays(1), new java.math.BigDecimal("100.00")); + seedOrderEarningLine(tenantId, clerkId, order1, new java.math.BigDecimal("100.00"), "available", EarningsType.ORDER); + String order2 = seedOrder(tenantId, clerkId, now.minusDays(2), new java.math.BigDecimal("50.00")); + seedOrderEarningLine(tenantId, clerkId, order2, new java.math.BigDecimal("50.00"), "available", EarningsType.ORDER); + + return R.ok(new E2eWageAdjustmentSeedResponse( + tenantKey, + adminUserName, + adminPassword, + clerkId, + clerk.getNickname(), + customer.getId(), + customer.getNickname(), + begin.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + end.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + "150.00")); + } + } + + @ApiOperation(value = "测试用:种子数据(工资调整,多店员)", notes = "创建 N 个店员 + 每人 2 个订单收益 line(amount>0, 有 order_id),用于 E2E 测试") + @PostMapping("/e2e/seed/wage-adjustment/multi") + public R seedWageAdjustmentMulti( + @RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth, + @RequestParam(value = "clerkCount", required = false, defaultValue = "2") int clerkCount) { + if (!isTestAuthHeaderValid(testAuth)) { + return rejectTestAuth(); + } + if (clerkCount < 1 || clerkCount > 5) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "clerkCount must be between 1 and 5"); + } + + final String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID; + final String tenantKey = ApiTestDataSeeder.DEFAULT_TENANT_KEY; + final String adminUserName = ApiTestDataSeeder.DEFAULT_ADMIN_USERNAME; + final String adminPassword = "apitest-secret"; + + try (TenantScope ignored = TenantScope.use(tenantId)) { + PlayClerkUserInfoEntity baseClerk = clerkUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CLERK_ID); + if (baseClerk == null) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "seed not ready"); + } + + LocalDateTime now = LocalDateTime.now().withNano(0); + LocalDateTime begin = now.minusDays(7); + LocalDateTime end = now.plusSeconds(1); + + List clerks = new ArrayList<>(); + clerks.add(new E2eSeedClerk(baseClerk.getId(), baseClerk.getNickname())); + + for (int i = 1; i < clerkCount; i++) { + String clerkId = "clerk-e2e-" + IdUtils.getUuid(); + PlayClerkUserInfoEntity clerk = new PlayClerkUserInfoEntity(); + clerk.setId(clerkId); + clerk.setTenantId(tenantId); + clerk.setOpenid("openid-" + clerkId); + // Ensure nickname is unique across repeated E2E runs, otherwise pc-tenant "filter by nickname" can pick wrong clerk. + clerk.setNickname("E2E-店员-" + (i + 1) + "-" + clerkId); + clerk.setGroupId(ApiTestDataSeeder.DEFAULT_GROUP_ID); + clerk.setLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID); + clerk.setOnboardingState("1"); + clerk.setListingState("1"); + clerk.setDisplayState("1"); + clerk.setRandomOrderState("1"); + clerk.setOnlineState("1"); + clerk.setClerkState("1"); + clerk.setDeleted(false); + Date nowDate = Date.from(LocalDateTime.now().atZone(java.time.ZoneId.systemDefault()).toInstant()); + clerk.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID); + clerk.setCreatedTime(nowDate); + clerk.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID); + clerk.setUpdatedTime(nowDate); + clerkUserInfoService.save(clerk); + + clerks.add(new E2eSeedClerk(clerkId, clerk.getNickname())); + } + + for (E2eSeedClerk clerk : clerks) { + String order1 = seedOrder(tenantId, clerk.getClerkId(), now.minusDays(1), new java.math.BigDecimal("100.00")); + seedOrderEarningLine(tenantId, clerk.getClerkId(), order1, new java.math.BigDecimal("100.00"), "available", EarningsType.ORDER); + String order2 = seedOrder(tenantId, clerk.getClerkId(), now.minusDays(2), new java.math.BigDecimal("50.00")); + seedOrderEarningLine(tenantId, clerk.getClerkId(), order2, new java.math.BigDecimal("50.00"), "available", EarningsType.ORDER); + } + + return R.ok(new E2eWageAdjustmentMultiSeedResponse( + tenantKey, + adminUserName, + adminPassword, + clerks, + begin.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + end.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))); + } + } + + @ApiOperation(value = "测试用:种子数据(订单端到端)", notes = "刷新顾客余额并返回默认顾客/店员/商品/礼物信息,用于 Playwright E2E") + @PostMapping("/e2e/seed/order") + public R seedOrderE2e( + @RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth) { + if (!isTestAuthHeaderValid(testAuth)) { + return rejectTestAuth(); + } + final String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID; + try (TenantScope ignored = TenantScope.use(tenantId)) { + PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CLERK_ID); + PlayCustomUserInfoEntity customer = customUserInfoService.getById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); + if (clerk == null || customer == null) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "seed not ready"); + } + customUserInfoService.lambdaUpdate() + .set(PlayCustomUserInfoEntity::getAccountBalance, ApiTestDataSeeder.E2E_CUSTOMER_BALANCE) + .eq(PlayCustomUserInfoEntity::getId, customer.getId()) + .update(); + return R.ok(new E2eOrderSeedResponse( + ApiTestDataSeeder.DEFAULT_TENANT_KEY, + customer.getId(), + customer.getNickname(), + clerk.getId(), + clerk.getNickname(), + clerk.getLevelId(), + clerk.getSex(), + ApiTestDataSeeder.DEFAULT_COMMODITY_ID, + ApiTestDataSeeder.DEFAULT_GIFT_ID)); + } + } + + @ApiOperation(value = "测试用:种子数据(顾客余额)", notes = "设置顾客余额,用于余额不足/支付分支 E2E") + @PostMapping("/e2e/seed/customer-balance") + public R seedCustomerBalance( + @RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth, + @RequestParam("customerId") String customerId, + @RequestParam("balance") String balance) { + if (!isTestAuthHeaderValid(testAuth)) { + return rejectTestAuth(); + } + if (!StringUtils.hasText(customerId) || !StringUtils.hasText(balance)) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "missing required fields"); + } + final String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID; + try (TenantScope ignored = TenantScope.use(tenantId)) { + java.math.BigDecimal amount; + try { + amount = new java.math.BigDecimal(balance); + } catch (Exception ex) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "invalid balance"); + } + boolean updated = customUserInfoService.lambdaUpdate() + .set(PlayCustomUserInfoEntity::getAccountBalance, amount) + .eq(PlayCustomUserInfoEntity::getId, customerId) + .update(); + if (!updated) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "customer not found"); + } + return R.ok(new E2eCustomerBalanceSeedResponse(customerId, amount.toPlainString())); + } + } + + @ApiOperation(value = "测试用:种子数据(服务价格)", notes = "更新 play_commodity_and_level_info.price,用于价格变更 E2E") + @PostMapping("/e2e/seed/commodity-price") + public R seedCommodityPrice( + @RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth, + @RequestParam("commodityId") String commodityId, + @RequestParam("levelId") String levelId, + @RequestParam("price") String price) { + if (!isTestAuthHeaderValid(testAuth)) { + return rejectTestAuth(); + } + if (!StringUtils.hasText(commodityId) || !StringUtils.hasText(levelId) || !StringUtils.hasText(price)) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "missing required fields"); + } + final String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID; + try (TenantScope ignored = TenantScope.use(tenantId)) { + java.math.BigDecimal amount; + try { + amount = new java.math.BigDecimal(price); + } catch (Exception ex) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "invalid price"); + } + PlayCommodityAndLevelInfoEntity mapping = commodityAndLevelInfoService.queryById(commodityId, levelId); + if (mapping == null) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "commodity/level mapping not found"); + } + mapping.setPrice(amount); + commodityAndLevelInfoService.updateById(mapping); + return R.ok(new E2eCommodityPriceSeedResponse(commodityId, levelId, amount.toPlainString())); + } + } + + @ApiOperation(value = "测试用:种子数据(店员上下架)", notes = "修改店员 listingState,用于下架/不可用分支 E2E") + @PostMapping("/e2e/seed/clerk-listing-state") + public R seedClerkListingState( + @RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth, + @RequestParam("clerkId") String clerkId, + @RequestParam("listingState") String listingState) { + if (!isTestAuthHeaderValid(testAuth)) { + return rejectTestAuth(); + } + if (!StringUtils.hasText(clerkId) || !StringUtils.hasText(listingState)) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "missing required fields"); + } + final String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID; + try (TenantScope ignored = TenantScope.use(tenantId)) { + PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(clerkId); + if (clerk == null) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "clerk not found"); + } + ListingStatus target = ListingStatus.fromCode(listingState); + clerk.setListingState(target.getCode()); + clerkUserInfoService.updateById(clerk); + return R.ok(new E2eClerkListingSeedResponse(clerkId, target.getCode())); + } + } + + @ApiOperation(value = "测试用:种子数据(冻结收益)", notes = "插入 frozen earning line(有 order_id, amount>0),用于冻结/不可提现分支 E2E") + @PostMapping("/e2e/seed/frozen-earnings") + public R seedFrozenEarnings( + @RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth, + @RequestParam("clerkId") String clerkId, + @RequestParam("amount") String amount) { + if (!isTestAuthHeaderValid(testAuth)) { + return rejectTestAuth(); + } + if (!StringUtils.hasText(clerkId) || !StringUtils.hasText(amount)) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "missing required fields"); + } + final String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID; + try (TenantScope ignored = TenantScope.use(tenantId)) { + java.math.BigDecimal value; + try { + value = new java.math.BigDecimal(amount); + } catch (Exception ex) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "invalid amount"); + } + if (value.signum() <= 0) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "amount must be > 0"); + } + PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(clerkId); + if (clerk == null) { + return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "clerk not found"); + } + LocalDateTime now = LocalDateTime.now().withNano(0); + String orderId = seedOrder(tenantId, clerkId, now.minusDays(1), value); + seedOrderEarningLine(tenantId, clerkId, orderId, value, "frozen", EarningsType.ORDER); + return R.ok(new E2eFrozenEarningsSeedResponse(clerkId, orderId, value.toPlainString())); + } + } + + @ApiOperation(value = "测试用:种子数据(tenantB)", notes = "创建 tenant-key-apitest-b 及其 admin/clerk,用于多租户隔离 E2E") + @PostMapping("/e2e/seed/tenant-b") + public R seedTenantB( + @RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth) { + if (!isTestAuthHeaderValid(testAuth)) { + return rejectTestAuth(); + } + final String tenantId = "tenant-apitest-b"; + final String tenantKey = "tenant-key-apitest-b"; + final String adminUserName = "user-apitest-admin-b"; + final String rawPassword = "apitest-secret"; + final String adminUserId = "user-apitest-admin-b"; + final String groupId = "group-apitest-b"; + final String clerkLevelId = "level-apitest-b"; + final String clerkId = "clerk-apitest-b"; + final String clerkNickname = "E2E-B-店员-" + clerkId; + final String customerId = "customer-apitest-b"; + final String customerNickname = "E2E-B-顾客-" + customerId; + + // sys_tenant is tenant-line ignored in most queries, no need for TenantScope. + SysTenantEntity tenant = tenantService.getById(tenantId); + if (tenant == null) { + SysTenantEntity entity = new SysTenantEntity(); + entity.setTenantId(tenantId); + entity.setTenantName("API Test Tenant B"); + entity.setTenantType("0"); + entity.setTenantStatus("0"); + entity.setTenantCode("apitest-b"); + entity.setTenantKey(tenantKey); + entity.setAppId("wx-apitest-appid-b"); + entity.setSecret("wx-apitest-secret-b"); + entity.setMchId("wx-apitest-mchid-b"); + entity.setMchKey("wx-apitest-mchkey-b"); + entity.setPackageId(ApiTestDataSeeder.DEFAULT_PACKAGE_ID); + entity.setTenantTime(new Date(System.currentTimeMillis() + 365L * 24 * 3600 * 1000)); + entity.setUserName(adminUserName); + entity.setUserPwd(passwordEncoder.encode(rawPassword)); + entity.setPhone("13800000001"); + entity.setEmail("b@x.cn"); + entity.setAddress("API Test Street B"); + tenantService.save(entity); + } + + SysUserEntity existingAdmin = sysUserService.selectUserByUserNameAndTenantId(adminUserName, tenantId); + if (existingAdmin == null) { + SysUserEntity admin = new SysUserEntity(); + admin.setUserId(adminUserId); + admin.setUserCode(adminUserName); + admin.setPassWord(passwordEncoder.encode(rawPassword)); + admin.setRealName("API Test Admin B"); + admin.setUserNickname("API Admin B"); + admin.setStatus(0); + admin.setUserType(1); + admin.setTenantId(tenantId); + admin.setMobile("13800000001"); + admin.setAddTime(LocalDateTime.now()); + admin.setSuperAdmin(Boolean.TRUE); + sysUserService.save(admin); + } + + try (TenantScope ignored = TenantScope.use(tenantId)) { + PlayPersonnelGroupInfoEntity group = personnelGroupInfoService.getById(groupId); + if (group == null) { + PlayPersonnelGroupInfoEntity entity = new PlayPersonnelGroupInfoEntity(); + entity.setId(groupId); + entity.setTenantId(tenantId); + entity.setSysUserId(adminUserId); + entity.setSysUserCode(adminUserName); + entity.setGroupName("测试小组-B"); + entity.setLeaderName("API Admin B"); + entity.setAddTime(LocalDateTime.now()); + personnelGroupInfoService.save(entity); + } else { + personnelGroupInfoService.lambdaUpdate() + .set(PlayPersonnelGroupInfoEntity::getSysUserId, adminUserId) + .set(PlayPersonnelGroupInfoEntity::getSysUserCode, adminUserName) + .set(PlayPersonnelGroupInfoEntity::getGroupName, "测试小组-B") + .set(PlayPersonnelGroupInfoEntity::getLeaderName, "API Admin B") + .eq(PlayPersonnelGroupInfoEntity::getId, groupId) + .update(); + } + + PlayClerkLevelInfoEntity level = clerkLevelInfoService.getById(clerkLevelId); + if (level == null) { + PlayClerkLevelInfoEntity entity = new PlayClerkLevelInfoEntity(); + entity.setId(clerkLevelId); + entity.setTenantId(tenantId); + entity.setName("基础等级-B"); + entity.setLevel(1); + entity.setFirstRegularRatio(60); + entity.setNotFirstRegularRatio(50); + entity.setFirstRandomRadio(55); + entity.setNotFirstRandomRadio(45); + entity.setFirstRewardRatio(40); + entity.setNotFirstRewardRatio(35); + entity.setOrderNumber(1L); + clerkLevelInfoService.save(entity); + } + + PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(clerkId); + if (clerk == null) { + PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity(); + entity.setId(clerkId); + entity.setTenantId(tenantId); + entity.setOpenid("openid-" + clerkId); + entity.setNickname(clerkNickname); + entity.setGroupId(groupId); + entity.setLevelId(clerkLevelId); + entity.setOnboardingState(OnboardingStatus.ACTIVE.getCode()); + entity.setListingState(ListingStatus.LISTED.getCode()); + entity.setDisplayState("1"); + entity.setRandomOrderState("1"); + entity.setOnlineState("1"); + entity.setClerkState(ClerkRoleStatus.CLERK.getCode()); + entity.setDeleted(false); + Date nowDate = Date.from(LocalDateTime.now().atZone(java.time.ZoneId.systemDefault()).toInstant()); + entity.setCreatedBy(adminUserId); + entity.setCreatedTime(nowDate); + entity.setUpdatedBy(adminUserId); + entity.setUpdatedTime(nowDate); + clerkUserInfoService.save(entity); + } else { + Date nowDate = Date.from(LocalDateTime.now().atZone(java.time.ZoneId.systemDefault()).toInstant()); + clerkUserInfoService.lambdaUpdate() + .set(PlayClerkUserInfoEntity::getOpenid, "openid-" + clerkId) + .set(PlayClerkUserInfoEntity::getNickname, clerkNickname) + .set(PlayClerkUserInfoEntity::getGroupId, groupId) + .set(PlayClerkUserInfoEntity::getLevelId, clerkLevelId) + .set(PlayClerkUserInfoEntity::getOnboardingState, OnboardingStatus.ACTIVE.getCode()) + .set(PlayClerkUserInfoEntity::getListingState, ListingStatus.LISTED.getCode()) + .set(PlayClerkUserInfoEntity::getDisplayState, "1") + .set(PlayClerkUserInfoEntity::getRandomOrderState, "1") + .set(PlayClerkUserInfoEntity::getOnlineState, "1") + .set(PlayClerkUserInfoEntity::getClerkState, ClerkRoleStatus.CLERK.getCode()) + .set(PlayClerkUserInfoEntity::getDeleted, Boolean.FALSE) + .set(PlayClerkUserInfoEntity::getUpdatedBy, adminUserId) + .set(PlayClerkUserInfoEntity::getUpdatedTime, nowDate) + .eq(PlayClerkUserInfoEntity::getId, clerkId) + .update(); + } + + PlayCustomUserInfoEntity customer = customUserInfoService.getById(customerId); + if (customer == null) { + PlayCustomUserInfoEntity entity = new PlayCustomUserInfoEntity(); + entity.setId(customerId); + entity.setTenantId(tenantId); + entity.setOpenid("openid-" + customerId); + entity.setNickname(customerNickname); + entity.setSex(1); + entity.setAccountBalance(new java.math.BigDecimal("1000.00")); + entity.setDeleted(Boolean.FALSE); + entity.setCreatedBy(adminUserId); + entity.setCreatedTime(new Date()); + entity.setUpdatedBy(adminUserId); + entity.setUpdatedTime(new Date()); + customUserInfoService.save(entity); + } else { + customUserInfoService.lambdaUpdate() + .set(PlayCustomUserInfoEntity::getOpenid, "openid-" + customerId) + .set(PlayCustomUserInfoEntity::getNickname, customerNickname) + .set(PlayCustomUserInfoEntity::getSex, 1) + .set(PlayCustomUserInfoEntity::getAccountBalance, new java.math.BigDecimal("1000.00")) + .set(PlayCustomUserInfoEntity::getDeleted, Boolean.FALSE) + .set(PlayCustomUserInfoEntity::getUpdatedBy, adminUserId) + .set(PlayCustomUserInfoEntity::getUpdatedTime, new Date()) + .eq(PlayCustomUserInfoEntity::getId, customerId) + .update(); + } + } + + return R.ok(new E2eTenantSeedResponse( + tenantId, + tenantKey, + adminUserName, + rawPassword, + clerkId, + clerkNickname, + customerId, + customerNickname)); + } + + private static final class E2eWageAdjustmentSeedResponse { + private final String tenantKey; + private final String adminUserName; + private final String adminPassword; + private final String clerkId; + private final String clerkNickname; + private final String customerId; + private final String customerNickname; + private final String beginTime; + private final String endTime; + private final String baseAmount; + + private E2eWageAdjustmentSeedResponse( + String tenantKey, + String adminUserName, + String adminPassword, + String clerkId, + String clerkNickname, + String customerId, + String customerNickname, + String beginTime, + String endTime, + String baseAmount) { + this.tenantKey = tenantKey; + this.adminUserName = adminUserName; + this.adminPassword = adminPassword; + this.clerkId = clerkId; + this.clerkNickname = clerkNickname; + this.customerId = customerId; + this.customerNickname = customerNickname; + this.beginTime = beginTime; + this.endTime = endTime; + this.baseAmount = baseAmount; + } + + public String getTenantKey() { + return tenantKey; + } + + public String getAdminUserName() { + return adminUserName; + } + + public String getAdminPassword() { + return adminPassword; + } + + public String getClerkId() { + return clerkId; + } + + public String getClerkNickname() { + return clerkNickname; + } + + public String getCustomerId() { + return customerId; + } + + public String getCustomerNickname() { + return customerNickname; + } + + public String getBeginTime() { + return beginTime; + } + + public String getEndTime() { + return endTime; + } + + public String getBaseAmount() { + return baseAmount; + } + } + + private static final class E2eCustomerBalanceSeedResponse { + private final String customerId; + private final String balance; + + private E2eCustomerBalanceSeedResponse(String customerId, String balance) { + this.customerId = customerId; + this.balance = balance; + } + + public String getCustomerId() { + return customerId; + } + + public String getBalance() { + return balance; + } + } + + private static final class E2eCommodityPriceSeedResponse { + private final String commodityId; + private final String levelId; + private final String price; + + private E2eCommodityPriceSeedResponse(String commodityId, String levelId, String price) { + this.commodityId = commodityId; + this.levelId = levelId; + this.price = price; + } + + public String getCommodityId() { + return commodityId; + } + + public String getLevelId() { + return levelId; + } + + public String getPrice() { + return price; + } + } + + private static final class E2eClerkListingSeedResponse { + private final String clerkId; + private final String listingState; + + private E2eClerkListingSeedResponse(String clerkId, String listingState) { + this.clerkId = clerkId; + this.listingState = listingState; + } + + public String getClerkId() { + return clerkId; + } + + public String getListingState() { + return listingState; + } + } + + private static final class E2eFrozenEarningsSeedResponse { + private final String clerkId; + private final String orderId; + private final String amount; + + private E2eFrozenEarningsSeedResponse(String clerkId, String orderId, String amount) { + this.clerkId = clerkId; + this.orderId = orderId; + this.amount = amount; + } + + public String getClerkId() { + return clerkId; + } + + public String getOrderId() { + return orderId; + } + + public String getAmount() { + return amount; + } + } + + private static final class E2eTenantSeedResponse { + private final String tenantId; + private final String tenantKey; + private final String adminUserName; + private final String adminPassword; + private final String clerkId; + private final String clerkNickname; + private final String customerId; + private final String customerNickname; + + private E2eTenantSeedResponse( + String tenantId, + String tenantKey, + String adminUserName, + String adminPassword, + String clerkId, + String clerkNickname, + String customerId, + String customerNickname) { + this.tenantId = tenantId; + this.tenantKey = tenantKey; + this.adminUserName = adminUserName; + this.adminPassword = adminPassword; + this.clerkId = clerkId; + this.clerkNickname = clerkNickname; + this.customerId = customerId; + this.customerNickname = customerNickname; + } + + public String getTenantId() { + return tenantId; + } + + public String getTenantKey() { + return tenantKey; + } + + public String getAdminUserName() { + return adminUserName; + } + + public String getAdminPassword() { + return adminPassword; + } + + public String getClerkId() { + return clerkId; + } + + public String getClerkNickname() { + return clerkNickname; + } + + public String getCustomerId() { + return customerId; + } + + public String getCustomerNickname() { + return customerNickname; + } + } + + private static final class E2eOrderSeedResponse { + private final String tenantKey; + private final String customerId; + private final String customerNickname; + private final String clerkId; + private final String clerkNickname; + private final String clerkLevelId; + private final String clerkSex; + private final String commodityId; + private final String giftId; + + private E2eOrderSeedResponse( + String tenantKey, + String customerId, + String customerNickname, + String clerkId, + String clerkNickname, + String clerkLevelId, + String clerkSex, + String commodityId, + String giftId) { + this.tenantKey = tenantKey; + this.customerId = customerId; + this.customerNickname = customerNickname; + this.clerkId = clerkId; + this.clerkNickname = clerkNickname; + this.clerkLevelId = clerkLevelId; + this.clerkSex = clerkSex; + this.commodityId = commodityId; + this.giftId = giftId; + } + + public String getTenantKey() { + return tenantKey; + } + + public String getCustomerId() { + return customerId; + } + + public String getCustomerNickname() { + return customerNickname; + } + + public String getClerkId() { + return clerkId; + } + + public String getClerkNickname() { + return clerkNickname; + } + + public String getClerkLevelId() { + return clerkLevelId; + } + + public String getClerkSex() { + return clerkSex; + } + + public String getCommodityId() { + return commodityId; + } + + public String getGiftId() { + return giftId; + } + } + + private static final class E2eSeedClerk { + private final String clerkId; + private final String clerkNickname; + + private E2eSeedClerk(String clerkId, String clerkNickname) { + this.clerkId = clerkId; + this.clerkNickname = clerkNickname; + } + + public String getClerkId() { + return clerkId; + } + + public String getClerkNickname() { + return clerkNickname; + } + } + + private static final class E2eWageAdjustmentMultiSeedResponse { + private final String tenantKey; + private final String adminUserName; + private final String adminPassword; + private final List clerks; + private final String beginTime; + private final String endTime; + + private E2eWageAdjustmentMultiSeedResponse( + String tenantKey, + String adminUserName, + String adminPassword, + List clerks, + String beginTime, + String endTime) { + this.tenantKey = tenantKey; + this.adminUserName = adminUserName; + this.adminPassword = adminPassword; + this.clerks = clerks; + this.beginTime = beginTime; + this.endTime = endTime; + } + + public String getTenantKey() { + return tenantKey; + } + + public String getAdminUserName() { + return adminUserName; + } + + public String getAdminPassword() { + return adminPassword; + } + + public List getClerks() { + return clerks; + } + + public String getBeginTime() { + return beginTime; + } + + public String getEndTime() { + return endTime; + } + } + + private String seedOrder(String tenantId, String clerkId, LocalDateTime endTime, java.math.BigDecimal estimatedRevenue) { + PlayOrderInfoEntity order = new PlayOrderInfoEntity(); + String id = "order-e2e-" + IdUtils.getUuid(); + order.setId(id); + order.setTenantId(tenantId); + order.setOrderNo("E2E-" + System.currentTimeMillis()); + order.setOrderStatus("3"); + order.setOrderType("2"); + order.setPlaceType("0"); + order.setRewardType("0"); + order.setAcceptBy(clerkId); + order.setPurchaserBy(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID); + order.setCommodityId(ApiTestDataSeeder.DEFAULT_COMMODITY_ID); + order.setOrderMoney(new java.math.BigDecimal("120.50")); + order.setFinalAmount(order.getOrderMoney()); + order.setEstimatedRevenue(estimatedRevenue); + order.setOrderSettlementState("1"); + order.setOrderEndTime(endTime); + order.setOrderSettlementTime(endTime); + Date nowDate = Date.from(LocalDateTime.now().atZone(java.time.ZoneId.systemDefault()).toInstant()); + order.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID); + order.setCreatedTime(nowDate); + order.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID); + order.setUpdatedTime(nowDate); + order.setDeleted(false); + orderInfoService.save(order); + return id; + } + + private void seedOrderEarningLine( + String tenantId, + String clerkId, + String orderId, + java.math.BigDecimal amount, + String status, + EarningsType earningType) { + EarningsLineEntity entity = new EarningsLineEntity(); + String id = "earn-e2e-" + IdUtils.getUuid(); + entity.setId(id); + entity.setTenantId(tenantId); + entity.setClerkId(clerkId); + entity.setOrderId(orderId); + entity.setSourceType(EarningsSourceType.ORDER); + entity.setSourceId(orderId); + entity.setAmount(amount); + entity.setEarningType(earningType); + entity.setStatus(status); + LocalDateTime now = LocalDateTime.now().withNano(0); + entity.setUnlockTime("frozen".equalsIgnoreCase(status) ? now.plusHours(24) : now.minusHours(1)); + Date nowDate = Date.from(LocalDateTime.now().atZone(java.time.ZoneId.systemDefault()).toInstant()); + entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID); + entity.setCreatedTime(nowDate); + entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID); + entity.setUpdatedTime(nowDate); + entity.setDeleted(false); + earningsService.save(entity); + } + @ApiOperation(value = "获取配置地址", notes = "获取微信JSAPI配置签名") @ApiResponses({@ApiResponse(code = 200, message = "操作成功", response = WxJsapiSignature.class)}) @PostMapping("/getConfigAddress") @@ -299,7 +1242,12 @@ public class WxOauthController { @ApiResponses({@ApiResponse(code = 200, message = "操作成功", response = JSONObject.class), @ApiResponse(code = 500, message = "用户不存在")}) @PostMapping("/custom/loginById") - public R loginById1(@ApiParam(value = "登录信息", required = true) @Validated @RequestBody WxUserLoginVo vo) { + public R loginById1( + @RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth, + @ApiParam(value = "登录信息", required = true) @Validated @RequestBody WxUserLoginVo vo) { + if (!isTestAuthHeaderValid(testAuth)) { + return rejectTestAuth(); + } PlayCustomUserInfoEntity entity = customUserInfoService.selectById(vo.getCode()); if (entity == null) { throw new CustomException("用户不存在"); @@ -310,7 +1258,7 @@ public class WxOauthController { JSONObject jsonObject = JSONObject.from(entity); String tokenValue = tokenService.createWxUserToken(entity.getId()); jsonObject.put("tokenValue", TOKEN_PREFIX + tokenValue); - jsonObject.put("tokenName", CLERK_USER_LOGIN_TOKEN); + jsonObject.put("tokenName", CUSTOM_USER_LOGIN_TOKEN); jsonObject.put("loginDate", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss")); customUserInfoService.updateTokenById(entity.getId(), tokenValue); return R.ok(jsonObject); diff --git a/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxCustomMpService.java b/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxCustomMpService.java index 064650e..6964f02 100644 --- a/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxCustomMpService.java +++ b/play-admin/src/main/java/com/starry/admin/modules/weichat/service/WxCustomMpService.java @@ -52,6 +52,9 @@ public class WxCustomMpService { @Resource private WxMpService wxMpService; + @Value("${wechat.subscribe-check-enabled:true}") + private boolean subscribeCheckEnabled; + @Resource private SysTenantServiceImpl tenantService; @Resource @@ -480,6 +483,9 @@ public class WxCustomMpService { if (StrUtil.isBlankIfStr(openId)) { throw new ServiceException("openId不能为空"); } + if (!subscribeCheckEnabled) { + return; + } try { WxMpUser wxMpUser = proxyWxMpService(tenantId).getUserService().userInfo(openId); if (!wxMpUser.getSubscribe()) { diff --git a/play-admin/src/main/resources/application-apitest.yml b/play-admin/src/main/resources/application-apitest.yml index c07fb4c..667fff1 100644 --- a/play-admin/src/main/resources/application-apitest.yml +++ b/play-admin/src/main/resources/application-apitest.yml @@ -78,7 +78,14 @@ apitest: user-header: X-Test-User defaults: tenant-id: tenant-apitest - user-id: apitest-user + # Must exist in DB. ApiTestDataSeeder seeds DEFAULT_ADMIN_USER_ID=user-apitest-admin. + user-id: user-apitest-admin roles: - ROLE_TESTER - permissions: [] + permissions: + - withdraw:deduction:create + - withdraw:deduction:read + +# E2E/ApiTest: skip real WeChat subscribe check to keep flows deterministic. +wechat: + subscribe-check-enabled: false diff --git a/play-admin/src/test/java/com/starry/admin/api/WxOauthAdminTestAuthApiTest.java b/play-admin/src/test/java/com/starry/admin/api/WxOauthAdminTestAuthApiTest.java new file mode 100644 index 0000000..4e4277d --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/api/WxOauthAdminTestAuthApiTest.java @@ -0,0 +1,52 @@ +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 org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "test.auth.secret=apitest-secret") +class WxOauthAdminTestAuthApiTest extends AbstractApiTest { + + private static final String TEST_AUTH_HEADER = "X-Test-Auth"; + private static final String TEST_AUTH_SECRET = "apitest-secret"; + + @Test + void adminLoginByUsernameRejectsWithoutSecretHeader() throws Exception { + mockMvc.perform(post("/wx/oauth2/admin/loginByUsername") + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT) + .header("User-Agent", "apitest") + .contentType(MediaType.APPLICATION_JSON) + .content("{" + + "\"userName\":\"" + ApiTestDataSeeder.DEFAULT_ADMIN_USERNAME + "\"," + + "\"passWord\":\"apitest-secret\"," + + "\"tenantKey\":\"" + ApiTestDataSeeder.DEFAULT_TENANT_KEY + "\"" + + "}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(403)); + } + + @Test + void adminLoginByUsernameReturnsTokenWhenSecretHeaderValid() throws Exception { + mockMvc.perform(post("/wx/oauth2/admin/loginByUsername") + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT) + .header("User-Agent", "apitest") + .header(TEST_AUTH_HEADER, TEST_AUTH_SECRET) + .contentType(MediaType.APPLICATION_JSON) + .content("{" + + "\"userName\":\"" + ApiTestDataSeeder.DEFAULT_ADMIN_USERNAME + "\"," + + "\"passWord\":\"apitest-secret\"," + + "\"tenantKey\":\"" + ApiTestDataSeeder.DEFAULT_TENANT_KEY + "\"" + + "}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.tokenHead").isNotEmpty()) + .andExpect(jsonPath("$.data.token").isNotEmpty()); + } +} diff --git a/play-admin/src/test/java/com/starry/admin/api/WxOauthE2eSeedOrderApiTest.java b/play-admin/src/test/java/com/starry/admin/api/WxOauthE2eSeedOrderApiTest.java new file mode 100644 index 0000000..f4c3030 --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/api/WxOauthE2eSeedOrderApiTest.java @@ -0,0 +1,51 @@ +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 org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "test.auth.secret=apitest-secret") +class WxOauthE2eSeedOrderApiTest extends AbstractApiTest { + + private static final String TEST_AUTH_HEADER = "X-Test-Auth"; + private static final String TEST_AUTH_SECRET = "apitest-secret"; + + @Test + void seedOrderRejectsWithoutSecretHeader() throws Exception { + mockMvc.perform(post("/wx/oauth2/e2e/seed/order") + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT) + .header("User-Agent", "apitest") + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(403)); + } + + @Test + void seedOrderReturnsFixtureWhenSecretHeaderValid() throws Exception { + mockMvc.perform(post("/wx/oauth2/e2e/seed/order") + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT) + .header("User-Agent", "apitest") + .header(TEST_AUTH_HEADER, TEST_AUTH_SECRET) + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.tenantKey").value(ApiTestDataSeeder.DEFAULT_TENANT_KEY)) + .andExpect(jsonPath("$.data.customerId").value(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)) + .andExpect(jsonPath("$.data.customerNickname").isNotEmpty()) + .andExpect(jsonPath("$.data.clerkId").value(ApiTestDataSeeder.DEFAULT_CLERK_ID)) + .andExpect(jsonPath("$.data.clerkNickname").isNotEmpty()) + .andExpect(jsonPath("$.data.clerkLevelId").value(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID)) + .andExpect(jsonPath("$.data.clerkSex").value("2")) + .andExpect(jsonPath("$.data.commodityId").value(ApiTestDataSeeder.DEFAULT_COMMODITY_ID)) + .andExpect(jsonPath("$.data.giftId").value(ApiTestDataSeeder.DEFAULT_GIFT_ID)); + } +} diff --git a/play-admin/src/test/java/com/starry/admin/api/WxOauthE2eSeedWageAdjustmentApiTest.java b/play-admin/src/test/java/com/starry/admin/api/WxOauthE2eSeedWageAdjustmentApiTest.java new file mode 100644 index 0000000..7cad65d --- /dev/null +++ b/play-admin/src/test/java/com/starry/admin/api/WxOauthE2eSeedWageAdjustmentApiTest.java @@ -0,0 +1,48 @@ +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 org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = "test.auth.secret=apitest-secret") +class WxOauthE2eSeedWageAdjustmentApiTest extends AbstractApiTest { + + private static final String TEST_AUTH_HEADER = "X-Test-Auth"; + private static final String TEST_AUTH_SECRET = "apitest-secret"; + + @Test + void seedWageAdjustmentRejectsWithoutSecretHeader() throws Exception { + mockMvc.perform(post("/wx/oauth2/e2e/seed/wage-adjustment") + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT) + .header("User-Agent", "apitest") + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(403)); + } + + @Test + void seedWageAdjustmentReturnsFixtureWhenSecretHeaderValid() throws Exception { + mockMvc.perform(post("/wx/oauth2/e2e/seed/wage-adjustment") + .header(USER_HEADER, DEFAULT_USER) + .header(TENANT_HEADER, DEFAULT_TENANT) + .header("User-Agent", "apitest") + .header(TEST_AUTH_HEADER, TEST_AUTH_SECRET) + .contentType(MediaType.APPLICATION_JSON) + .content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.tenantKey").value(ApiTestDataSeeder.DEFAULT_TENANT_KEY)) + .andExpect(jsonPath("$.data.adminUserName").value(ApiTestDataSeeder.DEFAULT_ADMIN_USERNAME)) + .andExpect(jsonPath("$.data.clerkId").value(ApiTestDataSeeder.DEFAULT_CLERK_ID)) + .andExpect(jsonPath("$.data.beginTime").isNotEmpty()) + .andExpect(jsonPath("$.data.endTime").isNotEmpty()) + .andExpect(jsonPath("$.data.baseAmount").value("150.00")); + } +}