feat(admin): 商品配置与系统公告管理接口,id 以字符串下发
- /admin/products 六个接口 + 校验(code 唯一、store_product_id 同平台唯一、 COIN_PACK 需 coin_amount>0、VIP 需 vip_days>0、VIP 只建不启) - /admin/notices 六个接口,与 C 端 /app/notices 共用 OpsNoticeService.pageForApp 一套筛选口径 - 权限点 product:list/product:update/notice:list/notice:update + 菜单 31/32/33(sql/data-phase1.sql 幂等) - 雪花 id 超过 JS 安全整数范围:id 以字符串下发,避免浏览器四舍五入导致回查 200002 - C 端商品查询收敛到 ProductService.findForStore,OrderService 不再自带一份 - 测试:module-commerce 33 / admin-api 16 / app-api 49 / worker 12,共 110 条全过
This commit is contained in:
1 parent
7513ac06bc
commit
8d19a0cc0a
24 files changed
+1761
-48
No files matched your search
@@ -36,6 +36,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@Tag(name = "后台-菜单管理", description = "菜单树维护"),
|
||||
@Tag(name = "后台-操作日志", description = "操作日志检索"),
|
||||
@Tag(name = "后台-订单管理", description = "订单检索、补发与退款"),
|
||||
@Tag(name = "后台-商品配置", description = "商品(金币包)配置与上下架"),
|
||||
@Tag(name = "后台-系统公告", description = "系统公告发布与管理(与 C 端口径一致)"),
|
||||
@Tag(name = "后台-权限点", description = "权限点查询"),
|
||||
@Tag(name = "后台-分集收费", description = "分集价格配置"),
|
||||
@Tag(name = "后台-对账", description = "对账任务与手动对账"),
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.controller;
|
||||
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.framework.log.OperationLog;
|
||||
import com.fruitshorts.system.service.OpsNoticeService;
|
||||
import com.fruitshorts.system.vo.NoticeQueryReqVO;
|
||||
import com.fruitshorts.system.vo.NoticeRespVO;
|
||||
import com.fruitshorts.system.vo.NoticeSaveReqVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 后台系统公告管理。
|
||||
*
|
||||
* <p>发布后的可见性与 C 端 `GET /app/notices` **共用同一套筛选实现**
|
||||
* (`OpsNoticeService.pageForApp`):`status=1` + `target ∈ {ALL, USER}` + 已到发布时间 + 未过期。
|
||||
*/
|
||||
@Tag(name = "后台-系统公告")
|
||||
@RestController
|
||||
@RequestMapping("/admin/notices")
|
||||
@RequiredArgsConstructor
|
||||
public class NoticeAdminController {
|
||||
|
||||
private final OpsNoticeService opsNoticeService;
|
||||
|
||||
@Operation(summary = "公告分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('notice:list')")
|
||||
public Result<PageResult<NoticeRespVO>> page(@Validated NoticeQueryReqVO req) {
|
||||
return Result.ok(opsNoticeService.pageForAdmin(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "公告详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('notice:list')")
|
||||
public Result<NoticeRespVO> detail(@PathVariable Long id) {
|
||||
return Result.ok(opsNoticeService.detail(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "新建公告(默认发布,C 端立即可见;返回新公告 id,字符串)")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('notice:update')")
|
||||
@OperationLog(module = "系统公告", action = "新建公告")
|
||||
public Result<String> create(@Valid @RequestBody NoticeSaveReqVO req) {
|
||||
// 雪花 id 用字符串回,避免前端拿到被四舍五入的 id
|
||||
return Result.ok(String.valueOf(opsNoticeService.create(req)));
|
||||
}
|
||||
|
||||
@Operation(summary = "编辑公告")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('notice:update')")
|
||||
@OperationLog(module = "系统公告", action = "编辑公告")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody NoticeSaveReqVO req) {
|
||||
opsNoticeService.update(id, req);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "发布 / 下架(下架后 C 端立即不可见)")
|
||||
@PutMapping("/{id}/status")
|
||||
@PreAuthorize("@ss.hasPermission('notice:update')")
|
||||
@OperationLog(module = "系统公告", action = "公告上下架")
|
||||
public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
opsNoticeService.updateStatus(id, status);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除公告")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('notice:update')")
|
||||
@OperationLog(module = "系统公告", action = "删除公告")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
opsNoticeService.delete(id);
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.controller;
|
||||
|
||||
import com.fruitshorts.commerce.service.ProductService;
|
||||
import com.fruitshorts.commerce.vo.ProductQueryReqVO;
|
||||
import com.fruitshorts.commerce.vo.ProductRespVO;
|
||||
import com.fruitshorts.commerce.vo.ProductSaveReqVO;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.framework.log.OperationLog;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 后台商品(金币包)配置。
|
||||
*
|
||||
* <p>商品是 C 端 `POST /app/orders/verify` 的发币依据(按 `store_product_id` 对齐商店商品),
|
||||
* 所以 `code` 与"同平台 store_product_id"的唯一性、以及 type 与金额的匹配都在服务端强校验。
|
||||
*/
|
||||
@Tag(name = "后台-商品配置")
|
||||
@RestController
|
||||
@RequestMapping("/admin/products")
|
||||
@RequiredArgsConstructor
|
||||
public class ProductAdminController {
|
||||
|
||||
private final ProductService productService;
|
||||
|
||||
@Operation(summary = "商品分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('product:list')")
|
||||
public Result<PageResult<ProductRespVO>> page(@Validated ProductQueryReqVO req) {
|
||||
return Result.ok(productService.page(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "商品详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('product:list')")
|
||||
public Result<ProductRespVO> detail(@PathVariable Long id) {
|
||||
return Result.ok(productService.detail(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "新建商品(默认下架,需显式上架;返回新商品 id,字符串)")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('product:update')")
|
||||
@OperationLog(module = "商品配置", action = "新建商品")
|
||||
public Result<String> create(@Valid @RequestBody ProductSaveReqVO req) {
|
||||
// 雪花 id 用字符串回,避免前端拿到被四舍五入的 id
|
||||
return Result.ok(String.valueOf(productService.create(req)));
|
||||
}
|
||||
|
||||
@Operation(summary = "编辑商品")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('product:update')")
|
||||
@OperationLog(module = "商品配置", action = "编辑商品")
|
||||
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody ProductSaveReqVO req) {
|
||||
productService.update(id, req);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "上架 / 下架(会员商品在订阅上线前不能启用)")
|
||||
@PutMapping("/{id}/status")
|
||||
@PreAuthorize("@ss.hasPermission('product:update')")
|
||||
@OperationLog(module = "商品配置", action = "商品上下架")
|
||||
public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
productService.updateStatus(id, status);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除商品")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('product:update')")
|
||||
@OperationLog(module = "商品配置", action = "删除商品")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
productService.delete(id);
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* 权限点注解检查:新增的写接口必须带 `@PreAuthorize`,且权限点与契约一致。
|
||||
*
|
||||
* <p>为什么用反射而不是起 Spring 上下文:{@code @PreAuthorize} 靠方法代理生效,
|
||||
* 切片测试(standalone MockMvc)不会走代理,跑出来是"永远通过"的假绿;
|
||||
* 直接检查注解,能真正确认"忘写权限点"这类问题(无权限点被拒的前提是**有**权限点)。
|
||||
*/
|
||||
class AdminPermissionAnnotationTest {
|
||||
|
||||
@Test
|
||||
void should_declare_expected_permissions_when_checking_new_admin_endpoints() {
|
||||
assertThat(permissionsOf(ProductAdminController.class))
|
||||
.containsExactlyInAnyOrder("product:list", "product:list",
|
||||
"product:update", "product:update", "product:update", "product:update");
|
||||
assertThat(permissionsOf(NoticeAdminController.class))
|
||||
.containsExactlyInAnyOrder("notice:list", "notice:list",
|
||||
"notice:update", "notice:update", "notice:update", "notice:update");
|
||||
}
|
||||
|
||||
/** 收集控制器里所有映射方法的 @PreAuthorize 权限点(缺失则记为 MISSING,便于断言直接看到)。 */
|
||||
private List<String> permissionsOf(Class<?> controller) {
|
||||
List<String> permissions = new ArrayList<>();
|
||||
for (Method method : controller.getDeclaredMethods()) {
|
||||
if (!isEndpoint(method)) {
|
||||
continue;
|
||||
}
|
||||
PreAuthorize annotation = method.getAnnotation(PreAuthorize.class);
|
||||
permissions.add(annotation == null ? "MISSING" : extract(annotation.value()));
|
||||
}
|
||||
return permissions;
|
||||
}
|
||||
|
||||
private boolean isEndpoint(Method method) {
|
||||
return method.isAnnotationPresent(GetMapping.class) || method.isAnnotationPresent(PostMapping.class)
|
||||
|| method.isAnnotationPresent(PutMapping.class) || method.isAnnotationPresent(DeleteMapping.class);
|
||||
}
|
||||
|
||||
/** 从 `@ss.hasPermission('product:list')` 里取出权限码。 */
|
||||
private String extract(String expression) {
|
||||
int start = expression.indexOf('\'');
|
||||
int end = expression.lastIndexOf('\'');
|
||||
return start >= 0 && end > start ? expression.substring(start + 1, end) : expression;
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.controller;
|
||||
|
||||
import com.fruitshorts.admin.support.ControllerContractSupport;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.system.service.OpsNoticeService;
|
||||
import com.fruitshorts.system.vo.NoticeRespVO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
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.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/** `/admin/notices` 接入层契约。 */
|
||||
class NoticeAdminControllerContractTest {
|
||||
|
||||
private OpsNoticeService opsNoticeService;
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
opsNoticeService = mock(OpsNoticeService.class);
|
||||
mockMvc = ControllerContractSupport.mockMvcOf(new NoticeAdminController(opsNoticeService));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_paged_notices_when_page_is_requested() throws Exception {
|
||||
NoticeRespVO vo = new NoticeRespVO();
|
||||
vo.setId(1L);
|
||||
vo.setTitle("开服公告");
|
||||
vo.setTarget("ALL");
|
||||
vo.setTargetText("全部用户");
|
||||
vo.setType("SYSTEM");
|
||||
vo.setTypeText("系统");
|
||||
vo.setStatus(1);
|
||||
vo.setStatusText("已发布");
|
||||
given(opsNoticeService.pageForAdmin(any())).willReturn(new PageResult<>(1L, List.of(vo)));
|
||||
|
||||
mockMvc.perform(get("/admin/notices/page").param("status", "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.list[0].title").value("开服公告"))
|
||||
.andExpect(jsonPath("$.data.list[0].statusText").value("已发布"))
|
||||
.andExpect(jsonPath("$.data.list[0].targetText").value("全部用户"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_new_id_when_creating_notice() throws Exception {
|
||||
given(opsNoticeService.create(any())).willReturn(66L);
|
||||
|
||||
mockMvc.perform(post("/admin/notices")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"title\":\"开服公告\",\"content\":\"欢迎\",\"target\":\"ALL\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
// id 以字符串回:雪花 id 直接下发数字会被浏览器四舍五入
|
||||
.andExpect(jsonPath("$.data").value("66"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_send_string_id_when_notice_id_exceeds_js_safe_integer() throws Exception {
|
||||
NoticeRespVO vo = new NoticeRespVO();
|
||||
vo.setId(2111011206851596290L);
|
||||
vo.setTitle("开服公告");
|
||||
given(opsNoticeService.pageForAdmin(any())).willReturn(new PageResult<>(1L, List.of(vo)));
|
||||
|
||||
mockMvc.perform(get("/admin/notices/page"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.list[0].id").value("2111011206851596290"))
|
||||
.andExpect(jsonPath("$.data.list[0].id").isString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_bad_request_when_title_is_missing() throws Exception {
|
||||
mockMvc.perform(post("/admin/notices")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"content\":\"没有标题\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST.getCode()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_map_not_found_when_notice_does_not_exist() throws Exception {
|
||||
given(opsNoticeService.detail(any())).willThrow(
|
||||
new ServiceException(ErrorCode.DATA_NOT_FOUND, "公告不存在"));
|
||||
|
||||
mockMvc.perform(get("/admin/notices/404"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.DATA_NOT_FOUND.getCode()))
|
||||
.andExpect(jsonPath("$.message").value("公告不存在"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_pass_status_when_publishing_or_taking_down_notice() throws Exception {
|
||||
mockMvc.perform(put("/admin/notices/9/status").param("status", "0"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
verify(opsNoticeService).updateStatus(eq(9L), eq(0));
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.admin.controller;
|
||||
|
||||
import com.fruitshorts.admin.support.ControllerContractSupport;
|
||||
import com.fruitshorts.commerce.service.ProductService;
|
||||
import com.fruitshorts.commerce.vo.ProductRespVO;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
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.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/** `/admin/products` 接入层契约:路由、参数绑定、统一响应体、错误码映射。 */
|
||||
class ProductAdminControllerContractTest {
|
||||
|
||||
private ProductService productService;
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
productService = mock(ProductService.class);
|
||||
mockMvc = ControllerContractSupport.mockMvcOf(new ProductAdminController(productService));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_paged_products_when_page_is_requested() throws Exception {
|
||||
ProductRespVO vo = new ProductRespVO();
|
||||
vo.setId(1L);
|
||||
vo.setCode("coin_100");
|
||||
vo.setName("100 金币");
|
||||
vo.setType("COIN_PACK");
|
||||
vo.setTypeText("金币包");
|
||||
vo.setStatus(1);
|
||||
vo.setStatusText("已上架");
|
||||
vo.setTotalCoin(100L);
|
||||
given(productService.page(any())).willReturn(new PageResult<>(1L, List.of(vo)));
|
||||
|
||||
mockMvc.perform(get("/admin/products/page").param("type", "COIN_PACK"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.list[0].code").value("coin_100"))
|
||||
// 状态中文由服务端给(前端不写死)
|
||||
.andExpect(jsonPath("$.data.list[0].statusText").value("已上架"))
|
||||
.andExpect(jsonPath("$.data.list[0].typeText").value("金币包"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_new_id_when_creating_product() throws Exception {
|
||||
given(productService.create(any())).willReturn(88L);
|
||||
|
||||
mockMvc.perform(post("/admin/products")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"code\":\"coin_100\",\"name\":\"100 金币\",\"coinAmount\":100}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
// id 以字符串回:雪花 id 直接下发数字会被浏览器四舍五入
|
||||
.andExpect(jsonPath("$.data").value("88"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_send_string_id_when_product_id_exceeds_js_safe_integer() throws Exception {
|
||||
ProductRespVO vo = new ProductRespVO();
|
||||
// 一个真实量级的雪花 id(> 2^53-1),前端按字符串原样回传才不会被四舍五入
|
||||
vo.setId(2111010131922784257L);
|
||||
vo.setCode("coin_big_id");
|
||||
given(productService.page(any())).willReturn(new PageResult<>(1L, List.of(vo)));
|
||||
|
||||
mockMvc.perform(get("/admin/products/page"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.list[0].id").value("2111010131922784257"))
|
||||
.andExpect(jsonPath("$.data.list[0].id").isString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_bad_request_when_code_is_missing() throws Exception {
|
||||
// @NotBlank 校验失败 → 全局异常处理器给出 400 信封
|
||||
mockMvc.perform(post("/admin/products")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"name\":\"没有编码\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST.getCode()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_map_data_exists_when_store_product_id_is_duplicated() throws Exception {
|
||||
given(productService.create(any())).willThrow(
|
||||
new ServiceException(ErrorCode.DATA_EXISTS, "该平台下商店商品 id 已被占用:coin_100"));
|
||||
|
||||
mockMvc.perform(post("/admin/products")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"code\":\"coin_x\",\"name\":\"X\",\"storeProductId\":\"coin_100\",\"coinAmount\":100}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.DATA_EXISTS.getCode()))
|
||||
.andExpect(jsonPath("$.message").value("该平台下商店商品 id 已被占用:coin_100"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_pass_status_when_updating_product_status() throws Exception {
|
||||
mockMvc.perform(put("/admin/products/8/status").param("status", "1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
|
||||
verify(productService).updateStatus(eq(8L), eq(1));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user