| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- package com.xuekairui.gateway;
- import org.junit.jupiter.api.*;
- import org.springframework.http.MediaType;
- import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
- import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
- /**
- * 运营管理接口测试
- * 覆盖:邀请配置管理、爬虫次数手动发放、发放记录查询
- */
- @DisplayName("运营管理接口测试")
- @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
- class AdminControllerTest extends GatewayBaseTest {
- private String adminToken() {
- return generateAccessToken(1L, "13800138000");
- }
- // ==================== 邀请配置管理 ====================
- @Test
- @Order(1)
- @DisplayName("GET /api/admin/invite/config - 应返回邀请配置")
- void getInviteConfig_shouldReturnConfig() throws Exception {
- mockMvc.perform(get("/api/admin/invite/config")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200))
- .andExpect(jsonPath("$.data.rewardCrawlerCount").isNumber())
- .andExpect(jsonPath("$.data.maxDailyReward").isNumber())
- .andExpect(jsonPath("$.data.inviteCodeExpireDays").isNumber())
- .andExpect(jsonPath("$.data.maxInvitePerDay").isNumber());
- }
- @Test
- @Order(2)
- @DisplayName("GET /api/admin/invite/config - 无Token应返回401")
- void getInviteConfig_withoutToken_shouldReturn401() throws Exception {
- mockMvc.perform(get("/api/admin/invite/config"))
- .andExpect(status().isUnauthorized());
- }
- @Test
- @Order(3)
- @DisplayName("GET /api/admin/invite/config - 配置应包含多渠道字段")
- void getInviteConfig_shouldContainMultiChannelFields() throws Exception {
- mockMvc.perform(get("/api/admin/invite/config")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data.status").isNumber());
- }
- @Test
- @Order(4)
- @DisplayName("PUT /api/admin/invite/config - 更新邀请配置")
- void updateInviteConfig_shouldSucceed() throws Exception {
- String body = """
- {
- "rewardCrawlerCount": 5,
- "maxDailyReward": 50,
- "inviteCodeExpireDays": 30,
- "maxInvitePerDay": 10,
- "landingTitle": "测试标题",
- "appName": "智价云(药店版)"
- }
- """;
- mockMvc.perform(put("/api/admin/invite/config")
- .header("Authorization", "Bearer " + adminToken())
- .contentType(MediaType.APPLICATION_JSON)
- .content(body))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200));
- }
- @Test
- @Order(5)
- @DisplayName("PUT /api/admin/invite/config - 更新含多渠道配置")
- void updateInviteConfig_withChannels_shouldSucceed() throws Exception {
- String body = """
- {
- "rewardCrawlerCount": 5,
- "maxDailyReward": 50,
- "inviteCodeExpireDays": 30,
- "maxInvitePerDay": 10,
- "appDownloadUrl": "https://download.example.com/app.exe",
- "miniappPath": "/pages/download/index",
- "miniappAppId": "wx1234567890",
- "wechatRedirectUrl": "https://mp.weixin.qq.com/xxx",
- "dingtalkAppId": "dingxxx",
- "feishuAppId": "feishuxxx",
- "landingTitle": "邀请你加入",
- "landingDesc": "高效数据采集",
- "appName": "智价云(药店版)"
- }
- """;
- mockMvc.perform(put("/api/admin/invite/config")
- .header("Authorization", "Bearer " + adminToken())
- .contentType(MediaType.APPLICATION_JSON)
- .content(body))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200));
- }
- @Test
- @Order(6)
- @DisplayName("PUT /api/admin/invite/config - 无Token应返回401")
- void updateInviteConfig_withoutToken_shouldReturn401() throws Exception {
- mockMvc.perform(put("/api/admin/invite/config")
- .contentType(MediaType.APPLICATION_JSON)
- .content("{}"))
- .andExpect(status().isUnauthorized());
- }
- @Test
- @Order(7)
- @DisplayName("GET /api/admin/invite/configs - 应返回分页配置列表")
- void listInviteConfigs_shouldReturnPage() throws Exception {
- mockMvc.perform(get("/api/admin/invite/configs")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200))
- .andExpect(jsonPath("$.data.records").isArray())
- .andExpect(jsonPath("$.data.total").isNumber())
- .andExpect(jsonPath("$.data.current").value(1))
- .andExpect(jsonPath("$.data.size").value(20));
- }
- @Test
- @Order(8)
- @DisplayName("GET /api/admin/invite/config/{id} - 根据ID获取配置")
- void getInviteConfigById_shouldReturnConfig() throws Exception {
- mockMvc.perform(get("/api/admin/invite/config/1")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200))
- .andExpect(jsonPath("$.data.id").value(1))
- .andExpect(jsonPath("$.data.rewardCrawlerCount").isNumber());
- }
- @Test
- @Order(9)
- @DisplayName("GET /api/admin/invite/config/{id} - 不存在应返回错误")
- void getInviteConfigById_notFound_shouldReturnError() throws Exception {
- mockMvc.perform(get("/api/admin/invite/config/99999")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").isNumber());
- }
- @Test
- @Order(10)
- @DisplayName("POST /api/admin/invite/config - 创建新配置")
- void createInviteConfig_shouldSucceed() throws Exception {
- String body = """
- {
- "rewardCrawlerCount": 3,
- "rewardType": "MEMBERSHIP",
- "rewardMonths": 2,
- "maxDailyReward": 30,
- "inviteCodeExpireDays": 15,
- "maxInvitePerDay": 5,
- "maxTotalInvites": 20,
- "landingTitle": "新配置标题",
- "appName": "智价云(药店版)"
- }
- """;
- mockMvc.perform(post("/api/admin/invite/config")
- .header("Authorization", "Bearer " + adminToken())
- .contentType(MediaType.APPLICATION_JSON)
- .content(body))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200))
- .andExpect(jsonPath("$.data.id").isNumber())
- .andExpect(jsonPath("$.data.landingTitle").value("新配置标题"));
- }
- @Test
- @Order(11)
- @DisplayName("PUT /api/admin/invite/config/{id} - 更新指定ID的配置")
- void updateInviteConfigById_shouldSucceed() throws Exception {
- String body = """
- {
- "rewardMonths": 3,
- "landingTitle": "更新后的标题"
- }
- """;
- mockMvc.perform(put("/api/admin/invite/config/1")
- .header("Authorization", "Bearer " + adminToken())
- .contentType(MediaType.APPLICATION_JSON)
- .content(body))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200));
- }
- @Test
- @Order(12)
- @DisplayName("PUT /api/admin/invite/config/{id} - 不存在应返回错误")
- void updateInviteConfigById_notFound_shouldReturnError() throws Exception {
- mockMvc.perform(put("/api/admin/invite/config/99999")
- .header("Authorization", "Bearer " + adminToken())
- .contentType(MediaType.APPLICATION_JSON)
- .content("{\"rewardMonths\": 3}"))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").isNumber());
- }
- @Test
- @Order(13)
- @DisplayName("DELETE /api/admin/invite/config/{id} - 删除配置")
- void deleteInviteConfig_shouldSucceed() throws Exception {
- // 先创建一个临时配置用于删除
- String createBody = """
- {
- "rewardMonths": 1,
- "landingTitle": "待删除配置"
- }
- """;
- String createResult = mockMvc.perform(post("/api/admin/invite/config")
- .header("Authorization", "Bearer " + adminToken())
- .contentType(MediaType.APPLICATION_JSON)
- .content(createBody))
- .andExpect(status().isOk())
- .andReturn().getResponse().getContentAsString();
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- int newId = mapper.readTree(createResult).get("data").get("id").asInt();
- mockMvc.perform(delete("/api/admin/invite/config/" + newId)
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200));
- // 再次查询应返回错误
- mockMvc.perform(get("/api/admin/invite/config/" + newId)
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").isNumber());
- }
- // ==================== 爬虫次数手动发放 ====================
- @Test
- @Order(10)
- @DisplayName("POST /api/admin/crawler/grant - 手动发放爬虫次数")
- void grantCrawlerQuota_shouldSucceed() throws Exception {
- String body = """
- {
- "userId": 1,
- "quotaCount": 50,
- "expireDays": 30,
- "remark": "测试发放"
- }
- """;
- mockMvc.perform(post("/api/admin/crawler/grant")
- .header("Authorization", "Bearer " + adminToken())
- .contentType(MediaType.APPLICATION_JSON)
- .content(body))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200));
- }
- @Test
- @Order(11)
- @DisplayName("POST /api/admin/crawler/grant - 无Token应返回401")
- void grantCrawlerQuota_withoutToken_shouldReturn401() throws Exception {
- mockMvc.perform(post("/api/admin/crawler/grant")
- .contentType(MediaType.APPLICATION_JSON)
- .content("{\"userId\":1,\"quotaCount\":50}"))
- .andExpect(status().isUnauthorized());
- }
- @Test
- @Order(12)
- @DisplayName("GET /api/admin/crawler/grants - 应返回发放记录列表")
- void listGrants_shouldReturnList() throws Exception {
- mockMvc.perform(get("/api/admin/crawler/grants")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200))
- .andExpect(jsonPath("$.data").isArray());
- }
- @Test
- @Order(13)
- @DisplayName("GET /api/admin/crawler/grants?userId=1 - 按用户筛选")
- void listGrants_byUserId_shouldFilter() throws Exception {
- mockMvc.perform(get("/api/admin/crawler/grants?userId=1")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data").isArray());
- }
- @Test
- @Order(14)
- @DisplayName("GET /api/admin/crawler/grants?grantType=ADMIN - 按类型筛选")
- void listGrants_byGrantType_shouldFilter() throws Exception {
- mockMvc.perform(get("/api/admin/crawler/grants?grantType=ADMIN")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data").isArray());
- }
- @Test
- @Order(15)
- @DisplayName("GET /api/admin/crawler/grants?grantType=INVITE - 查看邀请奖励记录")
- void listGrants_inviteType_shouldReturnList() throws Exception {
- mockMvc.perform(get("/api/admin/crawler/grants?grantType=INVITE")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data").isArray());
- }
- @Test
- @Order(16)
- @DisplayName("GET /api/admin/crawler/grants?grantType=PURCHASE - 查看购买奖励记录")
- void listGrants_purchaseType_shouldReturnList() throws Exception {
- mockMvc.perform(get("/api/admin/crawler/grants?grantType=PURCHASE")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data").isArray());
- }
- // ==================== 邀请记录查看 ====================
- @Test
- @Order(20)
- @DisplayName("GET /api/admin/invite/rewards?userId=1 - 查看用户邀请记录")
- void listInviteRewards_byUserId_shouldReturnList() throws Exception {
- mockMvc.perform(get("/api/admin/invite/rewards?userId=1")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200))
- .andExpect(jsonPath("$.data").isArray());
- }
- @Test
- @Order(21)
- @DisplayName("GET /api/admin/invite/rewards - 无userId应返回空列表")
- void listInviteRewards_noUserId_shouldReturnEmpty() throws Exception {
- mockMvc.perform(get("/api/admin/invite/rewards")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data").isArray());
- }
- @Test
- @Order(22)
- @DisplayName("GET /api/admin/invite/rewards - 无Token应返回401")
- void listInviteRewards_withoutToken_shouldReturn401() throws Exception {
- mockMvc.perform(get("/api/admin/invite/rewards"))
- .andExpect(status().isUnauthorized());
- }
- // ==================== 用户爬虫详情查询(新增)====================
- @Test
- @Order(30)
- @DisplayName("GET /api/admin/crawler/user-detail?userId=1 - 应返回用户爬虫详情")
- void getCrawlerUserDetail_shouldReturnDetail() throws Exception {
- mockMvc.perform(get("/api/admin/crawler/user-detail?userId=1")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200))
- .andExpect(jsonPath("$.data.userId").value(1))
- .andExpect(jsonPath("$.data.nickname").exists())
- .andExpect(jsonPath("$.data.phone").exists())
- .andExpect(jsonPath("$.data.todayTotalUsage").isNumber())
- .andExpect(jsonPath("$.data.monthlyTotalUsage").isNumber())
- .andExpect(jsonPath("$.data.platformStats").isArray());
- }
- @Test
- @Order(31)
- @DisplayName("GET /api/admin/crawler/user-detail - 平台统计应包含各字段")
- void getCrawlerUserDetail_platformStats_shouldContainFields() throws Exception {
- mockMvc.perform(get("/api/admin/crawler/user-detail?userId=1")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data.platformStats").isArray())
- .andExpect(result -> {
- // 解析JSON验证平台统计
- String content = result.getResponse().getContentAsString();
- com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
- com.fasterxml.jackson.databind.JsonNode json = mapper.readTree(content);
- com.fasterxml.jackson.databind.JsonNode stats = json.get("data").get("platformStats");
-
- // 如果有平台统计数据,验证每个元素的字段
- if (stats.isArray() && stats.size() > 0) {
- for (int i = 0; i < stats.size(); i++) {
- var stat = stats.get(i);
-
- // 验证必填字段存在且不为null
- assert stat.has("platformCode") && !stat.get("platformCode").isNull() :
- "platformCode不能为空";
- assert stat.has("platformName") && !stat.get("platformName").isNull() :
- "platformName不能为空";
- assert stat.has("todayUsage") && stat.get("todayUsage").isNumber() :
- "todayUsage必须是数字";
- assert stat.has("monthlyUsage") && stat.get("monthlyUsage").isNumber() :
- "monthlyUsage必须是数字";
- assert stat.has("totalUsage") && stat.get("totalUsage").isNumber() :
- "totalUsage必须是数字";
-
- // 验证数值>=0
- assert stat.get("todayUsage").asInt() >= 0 : "todayUsage不能为负数";
- assert stat.get("monthlyUsage").asInt() >= 0 : "monthlyUsage不能为负数";
- assert stat.get("totalUsage").asInt() >= 0 : "totalUsage不能为负数";
-
- // 验证平台代码有效(已知枚举或容错显示为“其他平台”)
- String platformCode = stat.get("platformCode").asText();
- String platformName = stat.get("platformName").asText();
- com.xuekairui.user.entity.CrawlerPlatform enumVal =
- com.xuekairui.user.entity.CrawlerPlatform.findByCode(platformCode);
- if (enumVal != null) {
- assert platformName.equals(enumVal.getName()) :
- "平台名称不匹配: " + platformName + " vs " + enumVal.getName();
- } else {
- assert platformName.startsWith("其他平台") :
- "未知平台应显示'其他平台(xxx)': " + platformName;
- }
- }
-
- // 验证按总使用次数降序排列
- if (stats.size() > 1) {
- for (int i = 1; i < stats.size(); i++) {
- int prevTotal = stats.get(i - 1).get("totalUsage").asInt();
- int currTotal = stats.get(i).get("totalUsage").asInt();
- assert prevTotal >= currTotal :
- "平台统计应按总使用次数降序排列,但索引" + (i-1) + "的" + prevTotal +
- " < 索引" + i + "的" + currTotal;
- }
- }
- }
- });
- }
- @Test
- @Order(32)
- @DisplayName("GET /api/admin/crawler/user-detail - 手机号应脱敏")
- void getCrawlerUserDetail_phoneShouldBeMasked() throws Exception {
- mockMvc.perform(get("/api/admin/crawler/user-detail?userId=1")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data.phone").value("138****8000"));
- }
- @Test
- @Order(33)
- @DisplayName("GET /api/admin/crawler/user-detail - 无Token应返回401")
- void getCrawlerUserDetail_withoutToken_shouldReturn401() throws Exception {
- mockMvc.perform(get("/api/admin/crawler/user-detail?userId=1"))
- .andExpect(status().isUnauthorized());
- }
- @Test
- @Order(34)
- @DisplayName("GET /api/admin/crawler/user-detail?userId=999 - 不存在的用户应返回错误")
- void getCrawlerUserDetail_nonExistentUser_shouldReturnError() throws Exception {
- mockMvc.perform(get("/api/admin/crawler/user-detail?userId=999")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").isNumber());
- }
- // ==================== 邀请转化统计(新增)====================
- @Test
- @Order(40)
- @DisplayName("GET /api/admin/invite/conversion-stats?userId=1 - 应返回转化统计")
- void getInviteConversionStats_shouldReturnStats() throws Exception {
- mockMvc.perform(get("/api/admin/invite/conversion-stats?userId=1")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.code").value(200))
- .andExpect(jsonPath("$.data.inviteCode").exists())
- .andExpect(jsonPath("$.data.clickedCount").isNumber())
- .andExpect(jsonPath("$.data.totalInvited").isNumber())
- .andExpect(jsonPath("$.data.registeredCount").isNumber())
- .andExpect(jsonPath("$.data.pendingCount").isNumber())
- .andExpect(jsonPath("$.data.conversionRate").isNumber())
- .andExpect(jsonPath("$.data.totalReward").isNumber());
- }
- @Test
- @Order(41)
- @DisplayName("GET /api/admin/invite/conversion-stats - 转化率应在0-100之间")
- void getInviteConversionStats_conversionRateShouldBeInRange() throws Exception {
- mockMvc.perform(get("/api/admin/invite/conversion-stats?userId=1")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data.conversionRate").value(org.hamcrest.Matchers.greaterThanOrEqualTo(0.0)))
- .andExpect(jsonPath("$.data.conversionRate").value(org.hamcrest.Matchers.lessThanOrEqualTo(100.0)));
- }
- @Test
- @Order(42)
- @DisplayName("GET /api/admin/invite/conversion-stats - 待注册人数应>=0")
- void getInviteConversionStats_pendingCountShouldBeNonNegative() throws Exception {
- mockMvc.perform(get("/api/admin/invite/conversion-stats?userId=1")
- .header("Authorization", "Bearer " + adminToken()))
- .andExpect(status().isOk())
- .andExpect(jsonPath("$.data.pendingCount").value(org.hamcrest.Matchers.greaterThanOrEqualTo(0)));
- }
- @Test
- @Order(43)
- @DisplayName("GET /api/admin/invite/conversion-stats - 无Token应返回401")
- void getInviteConversionStats_withoutToken_shouldReturn401() throws Exception {
- mockMvc.perform(get("/api/admin/invite/conversion-stats?userId=1"))
- .andExpect(status().isUnauthorized());
- }
- }
|