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:
jianghaifeng committed 2026-10-16 16:33:15 +08:00
1 parent 7513ac06bc
commit 8d19a0cc0a
24 files changed
+1761 -48

No files matched your search

@@ -2,48 +2,30 @@
package com.fruitshorts.app.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fruitshorts.app.vo.AppNoticeVO;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.system.dal.entity.OpsNotice;
import com.fruitshorts.system.dal.mapper.OpsNoticeMapper;
import com.fruitshorts.system.service.OpsNoticeService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
/**
* C 端系统公告(`GET /app/notices`,匿名,只读)。
*
* <p>口径:`status=1` + `target ∈ {ALL, USER}` + 已到发布时间 + 未过期;
* 按 `publish_at desc, id desc` 排序。**没有数据就是空列表**(不是 404)——
* 公告当前也没有后台产出方,客户端拿到空列表属正常状态。
* <p>筛选口径**不在本类实现**:统一走 `OpsNoticeService.pageForApp(...)` ——
* 后台公告管理(`/admin/notices`)用的是同一套条件,避免"后台发布后 C 端到底能不能看到"两处条件漂移。
*
* <p>没有数据返回空列表(不是 404);本类只负责把实体转成 C 端字段(不暴露后台字段)。
*/
@Service
@RequiredArgsConstructor
public class AppNoticeService {
private static final String TARGET_ALL = "ALL";
private static final String TARGET_USER = "USER";
private static final int MAX_PAGE_SIZE = 50;
private final OpsNoticeMapper opsNoticeMapper;
private final OpsNoticeService opsNoticeService;
public PageResult<AppNoticeVO> page(int pageIndex, int pageSize) {
LocalDateTime now = LocalDateTime.now();
Page<OpsNotice> page = new Page<>(pageIndex < 1 ? 1 : pageIndex,
pageSize < 1 ? 20 : Math.min(pageSize, MAX_PAGE_SIZE));
IPage<OpsNotice> result = opsNoticeMapper.selectPage(page, Wrappers.<OpsNotice>lambdaQuery()
.eq(OpsNotice::getStatus, 1)
.in(OpsNotice::getTarget, List.of(TARGET_ALL, TARGET_USER))
.and(w -> w.isNull(OpsNotice::getPublishAt).or().le(OpsNotice::getPublishAt, now))
.and(w -> w.isNull(OpsNotice::getExpireAt).or().gt(OpsNotice::getExpireAt, now))
.orderByDesc(OpsNotice::getPublishAt)
.orderByDesc(OpsNotice::getId));
return new PageResult<>(result.getTotal(), result.getRecords().stream().map(this::toVO).toList());
PageResult<OpsNotice> page = opsNoticeService.pageForApp(pageIndex, pageSize);
return new PageResult<>(page.getTotal(), page.getList().stream().map(this::toVO).toList());
}
private AppNoticeVO toVO(OpsNotice notice) {
@@ -12,6 +12,7 @@ import com.fruitshorts.commerce.dal.mapper.StoreReceiptMapper;
import com.fruitshorts.commerce.service.DisabledStoreVerifier;
import com.fruitshorts.commerce.service.EntitlementService;
import com.fruitshorts.commerce.service.OrderService;
import com.fruitshorts.commerce.service.ProductService;
import com.fruitshorts.commerce.service.StoreVerifier;
import com.fruitshorts.commerce.service.UnlockService;
import com.fruitshorts.commerce.service.WalletService;
@@ -65,8 +66,8 @@ class AppOrdersVerifyContractTest {
storeReceiptMapper = mock(StoreReceiptMapper.class);
verifier = new StubStoreVerifier();
OrderService orderService = new OrderService(ordersMapper, mock(OrderEventMapper.class),
productMapper, storeReceiptMapper, verifier, mock(WalletService.class),
mock(EntitlementService.class));
new ProductService(productMapper), storeReceiptMapper, verifier,
mock(WalletService.class), mock(EntitlementService.class));
mockMvc = ControllerContractSupport.mockMvcOf(new AppCommerceController(
mock(WalletService.class), mock(UnlockService.class), orderService));
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
@@ -131,7 +132,7 @@ class AppOrdersVerifyContractTest {
void should_keep_disabled_behaviour_when_store_is_not_configured() throws Exception {
// 默认实现(未配凭据):对外仍是"尚未配置",不产生订单
OrderService disabled = new OrderService(ordersMapper, mock(OrderEventMapper.class),
productMapper, storeReceiptMapper, new DisabledStoreVerifier(),
new ProductService(productMapper), storeReceiptMapper, new DisabledStoreVerifier(),
mock(WalletService.class), mock(EntitlementService.class));
MockMvc disabledMvc = ControllerContractSupport.mockMvcOf(new AppCommerceController(
mock(WalletService.class), mock(UnlockService.class), disabled));