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

@@ -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 = "对账任务与手动对账"),
@@ -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();
}
}
@@ -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();
}
}