fix(admin): 补齐 /api/admin 方法级鉴权并加兜底过滤器
ShopKey/SkipPriceAsin/QueryAsin/ShopManage/ProductCategory 五个 controller 此前无任何 require* 鉴权,SecurityConfig 全局 permitAll 且 nginx 无 ACL, 公网 18080 上可匿名读写店铺密钥等敏感数据。统一补 requireAdminOrInternal; 其中 ShopManage/SkipPrice/QueryAsin 原先信任调用方传入 operatorId/superAdmin, 改为从已认证 principal 推导,消除传 superAdmin=true 越权扩大数据范围的问题。 新增 AdminApiGuardFilter 对 /api/admin/** 兜底要求有效身份(JWT 或内部令牌), 防止后续新增端点遗漏鉴权再裸奔;支持 admin-guard-enabled/exempt-prefixes 配置。
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
package com.nanri.aiimage.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
|
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /api/admin/** 兜底鉴权过滤器。
|
||||||
|
*
|
||||||
|
* <p>SecurityConfig 全局 permitAll 且无路径级拦截,历史上有多个 /api/admin 控制器遗漏方法级
|
||||||
|
* require*(公网 18080 直连可达即匿名裸奔)。本过滤器作为第二道网:凡进入 /api/admin 的请求
|
||||||
|
* 必须具备"有效身份"(Java JWT,或可信内部令牌 X-Internal-Token + operatorId,复用
|
||||||
|
* AdminAuthSupport),否则直接返回与全局异常一致的 401 响应体(HTTP 200 + {success:false,code})。
|
||||||
|
*
|
||||||
|
* <p>只保证"已认证",管理员/菜单级校验仍由各控制器 require* 负责,避免拦截普通登录用户本可
|
||||||
|
* 访问的 /api/admin 接口(如权限菜单列表仅 requireUser)。
|
||||||
|
*
|
||||||
|
* <p>豁免项:OPTIONS 预检;以及 aiimage.security.admin-guard-exempt-prefixes 配置的前缀
|
||||||
|
* (逗号分隔,用于内部自动化/设备回调等确需匿名可达的端点)。紧急回退:
|
||||||
|
* aiimage.security.admin-guard-enabled=false 关闭本过滤器。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AdminApiGuardFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private static final String ADMIN_API_PREFIX = "/api/admin";
|
||||||
|
|
||||||
|
private final AdminAuthSupport adminAuthSupport;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Value("${aiimage.security.admin-guard-enabled:true}")
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@Value("${aiimage.security.admin-guard-exempt-prefixes:}")
|
||||||
|
private String exemptPrefixes;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
if (!enabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String uri = request.getRequestURI();
|
||||||
|
if (!(uri.equals(ADMIN_API_PREFIX) || uri.startsWith(ADMIN_API_PREFIX + "/"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (String prefix : exemptSet()) {
|
||||||
|
if (uri.startsWith(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
try {
|
||||||
|
adminAuthSupport.requireUserOrInternal(request);
|
||||||
|
} catch (BusinessException ex) {
|
||||||
|
ApiResponse<Void> body = ex.getCode() == null
|
||||||
|
? ApiResponse.fail(ex.getMessage())
|
||||||
|
: ApiResponse.fail(ex.getCode(), ex.getMessage());
|
||||||
|
log.warn("[admin-guard] {} {} rejected: {}", request.getMethod(), request.getRequestURI(), ex.getMessage());
|
||||||
|
response.setStatus(HttpServletResponse.SC_OK);
|
||||||
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
|
response.getWriter().write(objectMapper.writeValueAsString(body));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> exemptSet() {
|
||||||
|
if (exemptPrefixes == null || exemptPrefixes.isBlank()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
return Arrays.stream(exemptPrefixes.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(s -> !s.isEmpty())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-8
@@ -2,12 +2,14 @@ package com.nanri.aiimage.modules.productcategory.controller;
|
|||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
||||||
|
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||||
import com.nanri.aiimage.modules.productcategory.model.dto.ProductCategorySaveRequest;
|
import com.nanri.aiimage.modules.productcategory.model.dto.ProductCategorySaveRequest;
|
||||||
import com.nanri.aiimage.modules.productcategory.model.vo.ProductCategoryItemVo;
|
import com.nanri.aiimage.modules.productcategory.model.vo.ProductCategoryItemVo;
|
||||||
import com.nanri.aiimage.modules.productcategory.model.vo.ProductCategoryListVo;
|
import com.nanri.aiimage.modules.productcategory.model.vo.ProductCategoryListVo;
|
||||||
import com.nanri.aiimage.modules.productcategory.service.ProductCategoryService;
|
import com.nanri.aiimage.modules.productcategory.service.ProductCategoryService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@@ -35,34 +37,45 @@ public class ProductCategoryController {
|
|||||||
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
||||||
|
|
||||||
private final ProductCategoryService productCategoryService;
|
private final ProductCategoryService productCategoryService;
|
||||||
|
private final AdminAuthSupport adminAuthSupport;
|
||||||
|
|
||||||
@GetMapping("/product-categories")
|
@GetMapping("/product-categories")
|
||||||
@Operation(summary = "查询商品类目树")
|
@Operation(summary = "查询商品类目树")
|
||||||
public ApiResponse<ProductCategoryListVo> list(@RequestParam(value = "keyword", required = false) String keyword) {
|
public ApiResponse<ProductCategoryListVo> list(
|
||||||
|
HttpServletRequest request,
|
||||||
|
@RequestParam(value = "keyword", required = false) String keyword) {
|
||||||
|
adminAuthSupport.requireAdminOrInternal(request);
|
||||||
return ApiResponse.success(productCategoryService.list(keyword));
|
return ApiResponse.success(productCategoryService.list(keyword));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/product-categories/children")
|
@GetMapping("/product-categories/children")
|
||||||
@Operation(summary = "分页查询指定父级下的商品类目")
|
@Operation(summary = "分页查询指定父级下的商品类目")
|
||||||
public ApiResponse<ProductCategoryListVo> children(
|
public ApiResponse<ProductCategoryListVo> children(
|
||||||
|
HttpServletRequest request,
|
||||||
@RequestParam(value = "parentId", required = false) Long parentId,
|
@RequestParam(value = "parentId", required = false) Long parentId,
|
||||||
@RequestParam(value = "page", defaultValue = "1") Long page,
|
@RequestParam(value = "page", defaultValue = "1") Long page,
|
||||||
@RequestParam(value = "pageSize", defaultValue = "20") Long pageSize) {
|
@RequestParam(value = "pageSize", defaultValue = "20") Long pageSize) {
|
||||||
|
adminAuthSupport.requireAdminOrInternal(request);
|
||||||
return ApiResponse.success(productCategoryService.children(parentId, page, pageSize));
|
return ApiResponse.success(productCategoryService.children(parentId, page, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/product-categories/search")
|
@GetMapping("/product-categories/search")
|
||||||
@Operation(summary = "分页搜索商品类目")
|
@Operation(summary = "分页搜索商品类目")
|
||||||
public ApiResponse<ProductCategoryListVo> search(
|
public ApiResponse<ProductCategoryListVo> search(
|
||||||
|
HttpServletRequest request,
|
||||||
@RequestParam(value = "keyword", required = false) String keyword,
|
@RequestParam(value = "keyword", required = false) String keyword,
|
||||||
@RequestParam(value = "page", defaultValue = "1") Long page,
|
@RequestParam(value = "page", defaultValue = "1") Long page,
|
||||||
@RequestParam(value = "pageSize", defaultValue = "20") Long pageSize) {
|
@RequestParam(value = "pageSize", defaultValue = "20") Long pageSize) {
|
||||||
|
adminAuthSupport.requireAdminOrInternal(request);
|
||||||
return ApiResponse.success(productCategoryService.search(keyword, page, pageSize));
|
return ApiResponse.success(productCategoryService.search(keyword, page, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/product-categories/export")
|
@GetMapping("/product-categories/export")
|
||||||
@Operation(summary = "导出商品类目")
|
@Operation(summary = "导出商品类目")
|
||||||
public ResponseEntity<byte[]> export(@RequestParam(value = "keyword", required = false) String keyword) {
|
public ResponseEntity<byte[]> export(
|
||||||
|
HttpServletRequest request,
|
||||||
|
@RequestParam(value = "keyword", required = false) String keyword) {
|
||||||
|
adminAuthSupport.requireAdminOrInternal(request);
|
||||||
byte[] bytes = productCategoryService.export(keyword);
|
byte[] bytes = productCategoryService.export(keyword);
|
||||||
String filename = "product-categories-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
|
String filename = "product-categories-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
@@ -74,20 +87,29 @@ public class ProductCategoryController {
|
|||||||
|
|
||||||
@PostMapping("/product-category")
|
@PostMapping("/product-category")
|
||||||
@Operation(summary = "新增商品类目")
|
@Operation(summary = "新增商品类目")
|
||||||
public ApiResponse<ProductCategoryItemVo> create(@Valid @RequestBody ProductCategorySaveRequest request) {
|
public ApiResponse<ProductCategoryItemVo> create(
|
||||||
return ApiResponse.success("创建成功", productCategoryService.create(request));
|
HttpServletRequest request,
|
||||||
|
@Valid @RequestBody ProductCategorySaveRequest body) {
|
||||||
|
adminAuthSupport.requireAdminOrInternal(request);
|
||||||
|
return ApiResponse.success("创建成功", productCategoryService.create(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/product-category/{id}")
|
@PutMapping("/product-category/{id}")
|
||||||
@Operation(summary = "更新商品类目")
|
@Operation(summary = "更新商品类目")
|
||||||
public ApiResponse<ProductCategoryItemVo> update(@PathVariable Long id,
|
public ApiResponse<ProductCategoryItemVo> update(
|
||||||
@Valid @RequestBody ProductCategorySaveRequest request) {
|
HttpServletRequest request,
|
||||||
return ApiResponse.success("保存成功", productCategoryService.update(id, request));
|
@PathVariable Long id,
|
||||||
|
@Valid @RequestBody ProductCategorySaveRequest body) {
|
||||||
|
adminAuthSupport.requireAdminOrInternal(request);
|
||||||
|
return ApiResponse.success("保存成功", productCategoryService.update(id, body));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/product-category/{id}")
|
@DeleteMapping("/product-category/{id}")
|
||||||
@Operation(summary = "删除商品类目")
|
@Operation(summary = "删除商品类目")
|
||||||
public ApiResponse<Void> delete(@PathVariable Long id) {
|
public ApiResponse<Void> delete(
|
||||||
|
HttpServletRequest request,
|
||||||
|
@PathVariable Long id) {
|
||||||
|
adminAuthSupport.requireAdminOrInternal(request);
|
||||||
productCategoryService.delete(id);
|
productCategoryService.delete(id);
|
||||||
return ApiResponse.success("删除成功", null);
|
return ApiResponse.success("删除成功", null);
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-33
@@ -2,6 +2,8 @@ package com.nanri.aiimage.modules.shopkey.controller;
|
|||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
||||||
|
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||||
|
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.QueryAsinCountryUpdateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.QueryAsinCountryUpdateRequest;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.QueryAsinCreateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.QueryAsinCreateRequest;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportProgressVo;
|
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportProgressVo;
|
||||||
@@ -12,6 +14,7 @@ import com.nanri.aiimage.modules.shopkey.service.QueryAsinService;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@@ -40,18 +43,24 @@ public class QueryAsinController {
|
|||||||
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
||||||
|
|
||||||
private final QueryAsinService queryAsinService;
|
private final QueryAsinService queryAsinService;
|
||||||
|
private final AdminAuthSupport adminAuthSupport;
|
||||||
|
|
||||||
|
/** 解析当前操作者(JWT 或内部令牌通道),权限与数据范围一律以该身份为准。 */
|
||||||
|
private AdminUserEntity requireOperator(HttpServletRequest request) {
|
||||||
|
return adminAuthSupport.requireAdminOrInternal(request);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "分页查询 ASIN")
|
@Operation(summary = "分页查询 ASIN")
|
||||||
public ApiResponse<QueryAsinPageVo> page(
|
public ApiResponse<QueryAsinPageVo> page(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
|
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
|
||||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize,
|
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize,
|
||||||
@Parameter(description = "分组ID") @RequestParam(required = false) Long groupId,
|
@Parameter(description = "分组ID") @RequestParam(required = false) Long groupId,
|
||||||
@Parameter(description = "店铺名") @RequestParam(required = false) String shopName,
|
@Parameter(description = "店铺名") @RequestParam(required = false) String shopName,
|
||||||
@Parameter(description = "商品 ASIN") @RequestParam(required = false) String asin,
|
@Parameter(description = "商品 ASIN") @RequestParam(required = false) String asin,
|
||||||
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(required = false) String country,
|
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(required = false) String country) {
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
|
||||||
return ApiResponse.success(queryAsinService.page(
|
return ApiResponse.success(queryAsinService.page(
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -59,20 +68,21 @@ public class QueryAsinController {
|
|||||||
shopName,
|
shopName,
|
||||||
asin,
|
asin,
|
||||||
country,
|
country,
|
||||||
operatorId,
|
operator.getId(),
|
||||||
Boolean.TRUE.equals(superAdmin)));
|
isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/export")
|
@GetMapping("/export")
|
||||||
@Operation(summary = "导出查询 ASIN")
|
@Operation(summary = "导出查询 ASIN")
|
||||||
public ResponseEntity<byte[]> export(
|
public ResponseEntity<byte[]> export(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "分组ID") @RequestParam(required = false) Long groupId,
|
@Parameter(description = "分组ID") @RequestParam(required = false) Long groupId,
|
||||||
@Parameter(description = "店铺名") @RequestParam(required = false) String shopName,
|
@Parameter(description = "店铺名") @RequestParam(required = false) String shopName,
|
||||||
@Parameter(description = "商品 ASIN") @RequestParam(required = false) String asin,
|
@Parameter(description = "商品 ASIN") @RequestParam(required = false) String asin,
|
||||||
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(required = false) String country,
|
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(required = false) String country) {
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
byte[] bytes = queryAsinService.export(groupId, shopName, asin, country,
|
||||||
byte[] bytes = queryAsinService.export(groupId, shopName, asin, country, operatorId, Boolean.TRUE.equals(superAdmin));
|
operator.getId(), isSuper(operator));
|
||||||
String filename = "query-asin-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
|
String filename = "query-asin-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, DownloadHeaderUtil.contentDisposition(filename))
|
.header(HttpHeaders.CONTENT_DISPOSITION, DownloadHeaderUtil.contentDisposition(filename))
|
||||||
@@ -84,71 +94,81 @@ public class QueryAsinController {
|
|||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "新增或覆盖查询 ASIN")
|
@Operation(summary = "新增或覆盖查询 ASIN")
|
||||||
public ApiResponse<QueryAsinItemVo> create(
|
public ApiResponse<QueryAsinItemVo> create(
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
HttpServletRequest request,
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
|
@Valid @RequestBody QueryAsinCreateRequest body) {
|
||||||
@Valid @RequestBody QueryAsinCreateRequest request) {
|
AdminUserEntity operator = requireOperator(request);
|
||||||
return ApiResponse.success("保存成功",
|
return ApiResponse.success("保存成功",
|
||||||
queryAsinService.createOrUpdate(request, operatorId, Boolean.TRUE.equals(superAdmin)));
|
queryAsinService.createOrUpdate(body, operator.getId(), isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/import")
|
@PostMapping("/import")
|
||||||
@Operation(summary = "导入添加查询 ASIN")
|
@Operation(summary = "导入添加查询 ASIN")
|
||||||
public ApiResponse<QueryAsinImportStartVo> importExcel(
|
public ApiResponse<QueryAsinImportStartVo> importExcel(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "xlsx/xls 文件", required = true) @RequestParam("file") MultipartFile file,
|
@Parameter(description = "xlsx/xls 文件", required = true) @RequestParam("file") MultipartFile file,
|
||||||
@Parameter(description = "Excel 未提供分组时使用的分组ID") @RequestParam(required = false) Long groupId,
|
@Parameter(description = "Excel 未提供分组时使用的分组ID") @RequestParam(required = false) Long groupId) {
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
|
||||||
return ApiResponse.success("开始导入",
|
return ApiResponse.success("开始导入",
|
||||||
queryAsinService.startImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
|
queryAsinService.startImport(file, groupId, operator.getId(), isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/import/{importId}")
|
@GetMapping("/import/{importId}")
|
||||||
@Operation(summary = "查询导入添加进度")
|
@Operation(summary = "查询导入添加进度")
|
||||||
public ApiResponse<QueryAsinImportProgressVo> importProgress(@PathVariable String importId) {
|
public ApiResponse<QueryAsinImportProgressVo> importProgress(
|
||||||
|
HttpServletRequest request,
|
||||||
|
@Parameter(description = "导入任务 ID", required = true) @PathVariable String importId) {
|
||||||
|
requireOperator(request);
|
||||||
return ApiResponse.success(queryAsinService.getImportProgress(importId));
|
return ApiResponse.success(queryAsinService.getImportProgress(importId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/delete-import")
|
@PostMapping("/delete-import")
|
||||||
@Operation(summary = "导入删除查询 ASIN")
|
@Operation(summary = "导入删除查询 ASIN")
|
||||||
public ApiResponse<QueryAsinImportStartVo> deleteImportExcel(
|
public ApiResponse<QueryAsinImportStartVo> deleteImportExcel(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "xlsx/xls 文件", required = true) @RequestParam("file") MultipartFile file,
|
@Parameter(description = "xlsx/xls 文件", required = true) @RequestParam("file") MultipartFile file,
|
||||||
@Parameter(description = "Excel 未提供分组时使用的分组ID") @RequestParam(required = false) Long groupId,
|
@Parameter(description = "Excel 未提供分组时使用的分组ID") @RequestParam(required = false) Long groupId) {
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
|
||||||
return ApiResponse.success("开始删除",
|
return ApiResponse.success("开始删除",
|
||||||
queryAsinService.startDeleteImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
|
queryAsinService.startDeleteImport(file, groupId, operator.getId(), isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/delete-import/{importId}")
|
@GetMapping("/delete-import/{importId}")
|
||||||
@Operation(summary = "查询导入删除进度")
|
@Operation(summary = "查询导入删除进度")
|
||||||
public ApiResponse<QueryAsinImportProgressVo> deleteImportProgress(@PathVariable String importId) {
|
public ApiResponse<QueryAsinImportProgressVo> deleteImportProgress(
|
||||||
|
HttpServletRequest request,
|
||||||
|
@Parameter(description = "导入删除任务 ID", required = true) @PathVariable String importId) {
|
||||||
|
requireOperator(request);
|
||||||
return ApiResponse.success(queryAsinService.getDeleteImportProgress(importId));
|
return ApiResponse.success(queryAsinService.getDeleteImportProgress(importId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}/countries/{country}")
|
@PutMapping("/{id}/countries/{country}")
|
||||||
@Operation(summary = "编辑指定国家的查询 ASIN")
|
@Operation(summary = "编辑指定国家的查询 ASIN")
|
||||||
public ApiResponse<QueryAsinItemVo> updateCountry(
|
public ApiResponse<QueryAsinItemVo> updateCountry(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
|
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
|
||||||
@Parameter(description = "国家编码", required = true) @PathVariable String country,
|
@Parameter(description = "国家编码", required = true) @PathVariable String country,
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
@Valid @RequestBody QueryAsinCountryUpdateRequest body) {
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Valid @RequestBody QueryAsinCountryUpdateRequest request) {
|
|
||||||
return ApiResponse.success("保存成功", queryAsinService.updateCountry(
|
return ApiResponse.success("保存成功", queryAsinService.updateCountry(
|
||||||
id,
|
id,
|
||||||
country,
|
country,
|
||||||
request.getAsin(),
|
body.getAsin(),
|
||||||
operatorId,
|
operator.getId(),
|
||||||
Boolean.TRUE.equals(superAdmin)));
|
isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}/countries/{country}")
|
@DeleteMapping("/{id}/countries/{country}")
|
||||||
@Operation(summary = "删除指定国家的查询 ASIN")
|
@Operation(summary = "删除指定国家的查询 ASIN")
|
||||||
public ApiResponse<Void> deleteCountry(
|
public ApiResponse<Void> deleteCountry(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
|
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
|
||||||
@Parameter(description = "国家编码", required = true) @PathVariable String country,
|
@Parameter(description = "国家编码", required = true) @PathVariable String country) {
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
queryAsinService.deleteCountry(id, country, operator.getId(), isSuper(operator));
|
||||||
queryAsinService.deleteCountry(id, country, operatorId, Boolean.TRUE.equals(superAdmin));
|
|
||||||
return ApiResponse.success("删除成功", null);
|
return ApiResponse.success("删除成功", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isSuper(AdminUserEntity operator) {
|
||||||
|
return "super_admin".equals(adminAuthSupport.currentRole(operator));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-5
@@ -1,6 +1,7 @@
|
|||||||
package com.nanri.aiimage.modules.shopkey.controller;
|
package com.nanri.aiimage.modules.shopkey.controller;
|
||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
|
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.ShopKeyCreateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.ShopKeyCreateRequest;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.ShopKeyUpdateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.ShopKeyUpdateRequest;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.vo.ShopKeyItemVo;
|
import com.nanri.aiimage.modules.shopkey.model.vo.ShopKeyItemVo;
|
||||||
@@ -12,6 +13,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
@@ -31,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
public class ShopKeyController {
|
public class ShopKeyController {
|
||||||
|
|
||||||
private final ShopKeyService shopKeyService;
|
private final ShopKeyService shopKeyService;
|
||||||
|
private final AdminAuthSupport adminAuthSupport;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "分页查询店铺密钥", description = "分页查询店铺密钥列表。")
|
@Operation(summary = "分页查询店铺密钥", description = "分页查询店铺密钥列表。")
|
||||||
@@ -38,8 +41,10 @@ public class ShopKeyController {
|
|||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = ShopKeyPageVo.class)))
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = ShopKeyPageVo.class)))
|
||||||
})
|
})
|
||||||
public ApiResponse<ShopKeyPageVo> page(
|
public ApiResponse<ShopKeyPageVo> page(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
|
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
|
||||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize) {
|
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize) {
|
||||||
|
adminAuthSupport.requireAdminOrInternal(request);
|
||||||
return ApiResponse.success(shopKeyService.page(page, pageSize));
|
return ApiResponse.success(shopKeyService.page(page, pageSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +54,11 @@ public class ShopKeyController {
|
|||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "创建成功", content = @Content(schema = @Schema(implementation = ShopKeyItemVo.class))),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "创建成功", content = @Content(schema = @Schema(implementation = ShopKeyItemVo.class))),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "参数不合法")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "参数不合法")
|
||||||
})
|
})
|
||||||
public ApiResponse<ShopKeyItemVo> create(@Valid @RequestBody ShopKeyCreateRequest request) {
|
public ApiResponse<ShopKeyItemVo> create(
|
||||||
return ApiResponse.success("创建成功", shopKeyService.create(request));
|
HttpServletRequest request,
|
||||||
|
@Valid @RequestBody ShopKeyCreateRequest body) {
|
||||||
|
adminAuthSupport.requireAdminOrInternal(request);
|
||||||
|
return ApiResponse.success("创建成功", shopKeyService.create(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@@ -61,9 +69,11 @@ public class ShopKeyController {
|
|||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "记录不存在")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "记录不存在")
|
||||||
})
|
})
|
||||||
public ApiResponse<ShopKeyItemVo> update(
|
public ApiResponse<ShopKeyItemVo> update(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
|
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
|
||||||
@Valid @RequestBody ShopKeyUpdateRequest request) {
|
@Valid @RequestBody ShopKeyUpdateRequest body) {
|
||||||
return ApiResponse.success("更新成功", shopKeyService.update(id, request));
|
adminAuthSupport.requireAdminOrInternal(request);
|
||||||
|
return ApiResponse.success("更新成功", shopKeyService.update(id, body));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@@ -72,7 +82,10 @@ public class ShopKeyController {
|
|||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "删除成功"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "记录不存在")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "记录不存在")
|
||||||
})
|
})
|
||||||
public ApiResponse<Void> delete(@Parameter(description = "主键ID", required = true) @PathVariable Long id) {
|
public ApiResponse<Void> delete(
|
||||||
|
HttpServletRequest request,
|
||||||
|
@Parameter(description = "主键ID", required = true) @PathVariable Long id) {
|
||||||
|
adminAuthSupport.requireAdminOrInternal(request);
|
||||||
shopKeyService.delete(id);
|
shopKeyService.delete(id);
|
||||||
return ApiResponse.success("删除成功", null);
|
return ApiResponse.success("删除成功", null);
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-37
@@ -1,6 +1,8 @@
|
|||||||
package com.nanri.aiimage.modules.shopkey.controller;
|
package com.nanri.aiimage.modules.shopkey.controller;
|
||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
|
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||||
|
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageCreateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageCreateRequest;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupCreateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupCreateRequest;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupUpdateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupUpdateRequest;
|
||||||
@@ -17,6 +19,7 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -37,11 +40,18 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
@Tag(name = "店铺管理", description = "维护店铺信息,支持查询、新建、修改、删除")
|
@Tag(name = "店铺管理", description = "维护店铺信息,支持查询、新建、修改、删除")
|
||||||
public class ShopManageController {
|
public class ShopManageController {
|
||||||
|
|
||||||
@Value("${aiimage.security.internal-token:}")
|
|
||||||
private String internalToken;
|
|
||||||
|
|
||||||
private final ShopManageService shopManageService;
|
private final ShopManageService shopManageService;
|
||||||
private final ShopManageGroupService shopManageGroupService;
|
private final ShopManageGroupService shopManageGroupService;
|
||||||
|
private final AdminAuthSupport adminAuthSupport;
|
||||||
|
|
||||||
|
/** 解析当前操作者(JWT 或内部令牌通道),数据范围一律以该身份为准,不信任调用方传入的 operatorId/superAdmin。 */
|
||||||
|
private AdminUserEntity requireOperator(HttpServletRequest request) {
|
||||||
|
return adminAuthSupport.requireAdminOrInternal(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSuper(AdminUserEntity operator) {
|
||||||
|
return "super_admin".equals(adminAuthSupport.currentRole(operator));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "分页查询店铺", description = "支持按分组、店铺名、当前操作者可见范围筛选")
|
@Operation(summary = "分页查询店铺", description = "支持按分组、店铺名、当前操作者可见范围筛选")
|
||||||
@@ -52,92 +62,92 @@ public class ShopManageController {
|
|||||||
content = @Content(schema = @Schema(implementation = ShopManagePageVo.class)))
|
content = @Content(schema = @Schema(implementation = ShopManagePageVo.class)))
|
||||||
})
|
})
|
||||||
public ApiResponse<ShopManagePageVo> page(
|
public ApiResponse<ShopManagePageVo> page(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
|
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
|
||||||
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize,
|
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize,
|
||||||
@Parameter(description = "分组 ID") @RequestParam(required = false) Long groupId,
|
@Parameter(description = "分组 ID") @RequestParam(required = false) Long groupId,
|
||||||
@Parameter(description = "店铺名称") @RequestParam(required = false) String shopName,
|
@Parameter(description = "店铺名称") @RequestParam(required = false) String shopName) {
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
|
||||||
return ApiResponse.success(shopManageService.page(
|
return ApiResponse.success(shopManageService.page(
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
groupId,
|
groupId,
|
||||||
shopName,
|
shopName,
|
||||||
operatorId,
|
operator.getId(),
|
||||||
Boolean.TRUE.equals(superAdmin)));
|
isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "新增店铺", description = "创建店铺记录,并写入创建人")
|
@Operation(summary = "新增店铺", description = "创建店铺记录,并写入创建人")
|
||||||
public ApiResponse<ShopManageItemVo> create(
|
public ApiResponse<ShopManageItemVo> create(
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
HttpServletRequest request,
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
|
@Valid @RequestBody ShopManageCreateRequest body) {
|
||||||
@Valid @RequestBody ShopManageCreateRequest request) {
|
AdminUserEntity operator = requireOperator(request);
|
||||||
return ApiResponse.success("创建成功",
|
return ApiResponse.success("创建成功",
|
||||||
shopManageService.create(request, operatorId, Boolean.TRUE.equals(superAdmin)));
|
shopManageService.create(body, operator.getId(), isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@Operation(summary = "更新店铺", description = "按 ID 更新店铺")
|
@Operation(summary = "更新店铺", description = "按 ID 更新店铺")
|
||||||
public ApiResponse<ShopManageItemVo> update(
|
public ApiResponse<ShopManageItemVo> update(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
|
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
@Valid @RequestBody ShopManageUpdateRequest body) {
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Valid @RequestBody ShopManageUpdateRequest request) {
|
|
||||||
return ApiResponse.success("更新成功",
|
return ApiResponse.success("更新成功",
|
||||||
shopManageService.update(id, operatorId, Boolean.TRUE.equals(superAdmin), request));
|
shopManageService.update(id, operator.getId(), isSuper(operator), body));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@Operation(summary = "删除店铺", description = "按 ID 删除店铺")
|
@Operation(summary = "删除店铺", description = "按 ID 删除店铺")
|
||||||
public ApiResponse<Void> delete(
|
public ApiResponse<Void> delete(
|
||||||
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
|
HttpServletRequest request,
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
@Parameter(description = "主键 ID", required = true) @PathVariable Long id) {
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
AdminUserEntity operator = requireOperator(request);
|
||||||
shopManageService.delete(id, operatorId, Boolean.TRUE.equals(superAdmin));
|
shopManageService.delete(id, operator.getId(), isSuper(operator));
|
||||||
return ApiResponse.success("删除成功", null);
|
return ApiResponse.success("删除成功", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/groups")
|
@GetMapping("/groups")
|
||||||
@Operation(summary = "查询分组列表", description = "普通管理员返回当前可访问的分组,超级管理员返回全部")
|
@Operation(summary = "查询分组列表", description = "普通管理员返回当前可访问的分组,超级管理员返回全部")
|
||||||
public ApiResponse<java.util.List<ShopManageGroupItemVo>> listGroups(
|
public ApiResponse<java.util.List<ShopManageGroupItemVo>> listGroups(
|
||||||
@Parameter(description = "创建人用户ID") @RequestParam(required = false) Long createdById,
|
HttpServletRequest request,
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam(required = false) Long operatorId,
|
@Parameter(description = "创建人用户ID") @RequestParam(required = false) Long createdById) {
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
AdminUserEntity operator = requireOperator(request);
|
||||||
if (Boolean.TRUE.equals(superAdmin)) {
|
if (isSuper(operator)) {
|
||||||
return ApiResponse.success(shopManageGroupService.list(createdById));
|
return ApiResponse.success(shopManageGroupService.list(createdById));
|
||||||
}
|
}
|
||||||
return ApiResponse.success(shopManageGroupService.listAccessible(operatorId, false));
|
return ApiResponse.success(shopManageGroupService.listAccessible(operator.getId(), false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/groups")
|
@PostMapping("/groups")
|
||||||
@Operation(summary = "新增分组", description = "创建分组并写入创建人")
|
@Operation(summary = "新增分组", description = "创建分组并写入创建人")
|
||||||
public ApiResponse<ShopManageGroupItemVo> createGroup(
|
public ApiResponse<ShopManageGroupItemVo> createGroup(
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
HttpServletRequest request,
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
|
@Valid @RequestBody ShopManageGroupCreateRequest body) {
|
||||||
@Valid @RequestBody ShopManageGroupCreateRequest request) {
|
AdminUserEntity operator = requireOperator(request);
|
||||||
return ApiResponse.success("创建成功",
|
return ApiResponse.success("创建成功",
|
||||||
shopManageGroupService.create(request, operatorId, Boolean.TRUE.equals(superAdmin)));
|
shopManageGroupService.create(body, operator.getId(), isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/groups/{id}")
|
@PutMapping("/groups/{id}")
|
||||||
@Operation(summary = "更新分组", description = "普通管理员只能修改自己可访问的分组")
|
@Operation(summary = "更新分组", description = "普通管理员只能修改自己可访问的分组")
|
||||||
public ApiResponse<ShopManageGroupItemVo> updateGroup(
|
public ApiResponse<ShopManageGroupItemVo> updateGroup(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
|
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
@Valid @RequestBody ShopManageGroupUpdateRequest body) {
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Valid @RequestBody ShopManageGroupUpdateRequest request) {
|
|
||||||
return ApiResponse.success("更新成功",
|
return ApiResponse.success("更新成功",
|
||||||
shopManageGroupService.update(id, operatorId, Boolean.TRUE.equals(superAdmin), request));
|
shopManageGroupService.update(id, operator.getId(), isSuper(operator), body));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/groups/{id}")
|
@DeleteMapping("/groups/{id}")
|
||||||
@Operation(summary = "删除分组", description = "普通管理员只能删除自己可访问的分组")
|
@Operation(summary = "删除分组", description = "普通管理员只能删除自己可访问的分组")
|
||||||
public ApiResponse<Void> deleteGroup(
|
public ApiResponse<Void> deleteGroup(
|
||||||
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
|
HttpServletRequest request,
|
||||||
@Parameter(description = "当前操作人用户ID") @RequestParam Long operatorId,
|
@Parameter(description = "主键 ID", required = true) @PathVariable Long id) {
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(defaultValue = "false") Boolean superAdmin) {
|
AdminUserEntity operator = requireOperator(request);
|
||||||
shopManageGroupService.delete(id, operatorId, Boolean.TRUE.equals(superAdmin));
|
shopManageGroupService.delete(id, operator.getId(), isSuper(operator));
|
||||||
return ApiResponse.success("删除成功", null);
|
return ApiResponse.success("删除成功", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,4 +161,17 @@ public class ShopManageController {
|
|||||||
}
|
}
|
||||||
return ApiResponse.success(shopManageService.getCredentialByShopName(shopName));
|
return ApiResponse.success(shopManageService.getCredentialByShopName(shopName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/credential")
|
||||||
|
@Operation(summary = "查看店铺明文凭据(管理端)", description = "管理员在后台查看店铺明文凭据;受管理员身份保护(建议后续补充归属/角色细粒度校验)")
|
||||||
|
public ApiResponse<ShopManageCredentialVo> adminCredential(
|
||||||
|
HttpServletRequest request,
|
||||||
|
@Parameter(description = "店铺主键 ID") @PathVariable Long id,
|
||||||
|
@Parameter(description = "店铺名") @RequestParam("shop_name") String shopName) {
|
||||||
|
requireOperator(request);
|
||||||
|
return ApiResponse.success(shopManageService.getCredentialByShopName(shopName));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value("${aiimage.security.internal-token:}")
|
||||||
|
private String internalToken;
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-33
@@ -2,6 +2,8 @@ package com.nanri.aiimage.modules.shopkey.controller;
|
|||||||
|
|
||||||
import com.nanri.aiimage.common.api.ApiResponse;
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
import com.nanri.aiimage.common.util.DownloadHeaderUtil;
|
||||||
|
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||||
|
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCountryUpdateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCountryUpdateRequest;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
|
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
|
||||||
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportProgressVo;
|
import com.nanri.aiimage.modules.shopkey.model.vo.QueryAsinImportProgressVo;
|
||||||
@@ -12,6 +14,7 @@ import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@@ -31,7 +34,6 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -42,10 +44,17 @@ public class SkipPriceAsinController {
|
|||||||
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
private static final DateTimeFormatter EXPORT_FILENAME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
||||||
|
|
||||||
private final SkipPriceAsinService skipPriceAsinService;
|
private final SkipPriceAsinService skipPriceAsinService;
|
||||||
|
private final AdminAuthSupport adminAuthSupport;
|
||||||
|
|
||||||
|
/** 解析当前操作者(JWT 或内部令牌通道),权限与数据范围一律以该身份为准。 */
|
||||||
|
private AdminUserEntity requireOperator(HttpServletRequest request) {
|
||||||
|
return adminAuthSupport.requireAdminOrInternal(request);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Operation(summary = "分页查询跳过跟价 ASIN", description = "支持按分组、店铺名、ASIN 和当前操作人可见范围筛选。")
|
@Operation(summary = "分页查询跳过跟价 ASIN", description = "支持按分组、店铺名、ASIN 和当前操作人可见范围筛选。")
|
||||||
public ApiResponse<SkipPriceAsinPageVo> page(
|
public ApiResponse<SkipPriceAsinPageVo> page(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "页码") @RequestParam(name = "page", defaultValue = "1") Long page,
|
@Parameter(description = "页码") @RequestParam(name = "page", defaultValue = "1") Long page,
|
||||||
@Parameter(description = "每页数量") @RequestParam(name = "page_size", defaultValue = "15") Long pageSize,
|
@Parameter(description = "每页数量") @RequestParam(name = "page_size", defaultValue = "15") Long pageSize,
|
||||||
@Parameter(description = "分组 ID") @RequestParam(name = "group_id", required = false) Long groupId,
|
@Parameter(description = "分组 ID") @RequestParam(name = "group_id", required = false) Long groupId,
|
||||||
@@ -53,9 +62,8 @@ public class SkipPriceAsinController {
|
|||||||
@Parameter(description = "商品 ASIN") @RequestParam(name = "asin", required = false) String asin,
|
@Parameter(description = "商品 ASIN") @RequestParam(name = "asin", required = false) String asin,
|
||||||
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(name = "country", required = false) String country,
|
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(name = "country", required = false) String country,
|
||||||
@Parameter(description = "最低价下限") @RequestParam(name = "minimum_price_from", required = false) BigDecimal minimumPriceFrom,
|
@Parameter(description = "最低价下限") @RequestParam(name = "minimum_price_from", required = false) BigDecimal minimumPriceFrom,
|
||||||
@Parameter(description = "最低价上限") @RequestParam(name = "minimum_price_to", required = false) BigDecimal minimumPriceTo,
|
@Parameter(description = "最低价上限") @RequestParam(name = "minimum_price_to", required = false) BigDecimal minimumPriceTo) {
|
||||||
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id", required = false) Long operatorId,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
|
|
||||||
return ApiResponse.success(skipPriceAsinService.page(
|
return ApiResponse.success(skipPriceAsinService.page(
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -65,23 +73,23 @@ public class SkipPriceAsinController {
|
|||||||
country,
|
country,
|
||||||
minimumPriceFrom,
|
minimumPriceFrom,
|
||||||
minimumPriceTo,
|
minimumPriceTo,
|
||||||
operatorId,
|
operator.getId(),
|
||||||
Boolean.TRUE.equals(superAdmin)));
|
isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/export")
|
@GetMapping("/export")
|
||||||
@Operation(summary = "导出跳过跟价 ASIN", description = "按当前查询条件导出 Excel。")
|
@Operation(summary = "导出跳过跟价 ASIN", description = "按当前查询条件导出 Excel。")
|
||||||
public ResponseEntity<byte[]> export(
|
public ResponseEntity<byte[]> export(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "分组 ID") @RequestParam(name = "group_id", required = false) Long groupId,
|
@Parameter(description = "分组 ID") @RequestParam(name = "group_id", required = false) Long groupId,
|
||||||
@Parameter(description = "店铺名称") @RequestParam(name = "shop_name", required = false) String shopName,
|
@Parameter(description = "店铺名称") @RequestParam(name = "shop_name", required = false) String shopName,
|
||||||
@Parameter(description = "商品 ASIN") @RequestParam(name = "asin", required = false) String asin,
|
@Parameter(description = "商品 ASIN") @RequestParam(name = "asin", required = false) String asin,
|
||||||
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(name = "country", required = false) String country,
|
@Parameter(description = "国家代码(DE/UK/FR/IT/ES)") @RequestParam(name = "country", required = false) String country,
|
||||||
@Parameter(description = "最低价下限") @RequestParam(name = "minimum_price_from", required = false) BigDecimal minimumPriceFrom,
|
@Parameter(description = "最低价下限") @RequestParam(name = "minimum_price_from", required = false) BigDecimal minimumPriceFrom,
|
||||||
@Parameter(description = "最低价上限") @RequestParam(name = "minimum_price_to", required = false) BigDecimal minimumPriceTo,
|
@Parameter(description = "最低价上限") @RequestParam(name = "minimum_price_to", required = false) BigDecimal minimumPriceTo) {
|
||||||
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id", required = false) Long operatorId,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
|
|
||||||
byte[] bytes = skipPriceAsinService.export(groupId, shopName, asin, country, minimumPriceFrom, minimumPriceTo,
|
byte[] bytes = skipPriceAsinService.export(groupId, shopName, asin, country, minimumPriceFrom, minimumPriceTo,
|
||||||
operatorId, Boolean.TRUE.equals(superAdmin));
|
operator.getId(), isSuper(operator));
|
||||||
String filename = "skip-price-asin-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
|
String filename = "skip-price-asin-" + LocalDateTime.now().format(EXPORT_FILENAME_FORMATTER) + ".xlsx";
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, DownloadHeaderUtil.contentDisposition(filename))
|
.header(HttpHeaders.CONTENT_DISPOSITION, DownloadHeaderUtil.contentDisposition(filename))
|
||||||
@@ -93,74 +101,82 @@ public class SkipPriceAsinController {
|
|||||||
@PostMapping
|
@PostMapping
|
||||||
@Operation(summary = "新增跳过跟价 ASIN", description = "新增一条跳过跟价 ASIN 记录。")
|
@Operation(summary = "新增跳过跟价 ASIN", description = "新增一条跳过跟价 ASIN 记录。")
|
||||||
public ApiResponse<SkipPriceAsinItemVo> create(
|
public ApiResponse<SkipPriceAsinItemVo> create(
|
||||||
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id") Long operatorId,
|
HttpServletRequest request,
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin,
|
@Valid @RequestBody SkipPriceAsinCreateRequest body) {
|
||||||
@Valid @RequestBody SkipPriceAsinCreateRequest request) {
|
AdminUserEntity operator = requireOperator(request);
|
||||||
return ApiResponse.success("保存成功",
|
return ApiResponse.success("保存成功",
|
||||||
skipPriceAsinService.create(request, operatorId, Boolean.TRUE.equals(superAdmin)));
|
skipPriceAsinService.create(body, operator.getId(), isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/import")
|
@PostMapping("/import")
|
||||||
@Operation(summary = "导入跳过跟价 ASIN", description = "上传 xlsx 或 xls 文件,异步导入新增数据。")
|
@Operation(summary = "导入跳过跟价 ASIN", description = "上传 xlsx 或 xls 文件,异步导入新增数据。")
|
||||||
public ApiResponse<QueryAsinImportStartVo> importExcel(
|
public ApiResponse<QueryAsinImportStartVo> importExcel(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "xlsx 或 xls 文件", required = true) @RequestParam("file") MultipartFile file,
|
@Parameter(description = "xlsx 或 xls 文件", required = true) @RequestParam("file") MultipartFile file,
|
||||||
@Parameter(description = "分组 ID", required = true) @RequestParam(name = "group_id") Long groupId,
|
@Parameter(description = "分组 ID", required = true) @RequestParam(name = "group_id") Long groupId) {
|
||||||
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id") Long operatorId,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
|
|
||||||
return ApiResponse.success("开始导入",
|
return ApiResponse.success("开始导入",
|
||||||
skipPriceAsinService.startImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
|
skipPriceAsinService.startImport(file, groupId, operator.getId(), isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/import/{importId}")
|
@GetMapping("/import/{importId}")
|
||||||
@Operation(summary = "查询导入进度")
|
@Operation(summary = "查询导入进度")
|
||||||
public ApiResponse<QueryAsinImportProgressVo> importProgress(
|
public ApiResponse<QueryAsinImportProgressVo> importProgress(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "导入任务 ID", required = true) @PathVariable String importId) {
|
@Parameter(description = "导入任务 ID", required = true) @PathVariable String importId) {
|
||||||
|
requireOperator(request);
|
||||||
return ApiResponse.success(skipPriceAsinService.getImportProgress(importId));
|
return ApiResponse.success(skipPriceAsinService.getImportProgress(importId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/delete-import")
|
@PostMapping("/delete-import")
|
||||||
@Operation(summary = "导入删除跳过跟价 ASIN", description = "上传 xlsx 或 xls 文件,异步按文件内容删除数据。")
|
@Operation(summary = "导入删除跳过跟价 ASIN", description = "上传 xlsx 或 xls 文件,异步按文件内容删除数据。")
|
||||||
public ApiResponse<QueryAsinImportStartVo> deleteImportExcel(
|
public ApiResponse<QueryAsinImportStartVo> deleteImportExcel(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "xlsx 或 xls 文件", required = true) @RequestParam("file") MultipartFile file,
|
@Parameter(description = "xlsx 或 xls 文件", required = true) @RequestParam("file") MultipartFile file,
|
||||||
@Parameter(description = "分组 ID", required = true) @RequestParam(name = "group_id") Long groupId,
|
@Parameter(description = "分组 ID", required = true) @RequestParam(name = "group_id") Long groupId) {
|
||||||
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id") Long operatorId,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
|
|
||||||
return ApiResponse.success("开始删除",
|
return ApiResponse.success("开始删除",
|
||||||
skipPriceAsinService.startDeleteImport(file, groupId, operatorId, Boolean.TRUE.equals(superAdmin)));
|
skipPriceAsinService.startDeleteImport(file, groupId, operator.getId(), isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/delete-import/{importId}")
|
@GetMapping("/delete-import/{importId}")
|
||||||
@Operation(summary = "查询导入删除进度")
|
@Operation(summary = "查询导入删除进度")
|
||||||
public ApiResponse<QueryAsinImportProgressVo> deleteImportProgress(
|
public ApiResponse<QueryAsinImportProgressVo> deleteImportProgress(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "导入删除任务 ID", required = true) @PathVariable String importId) {
|
@Parameter(description = "导入删除任务 ID", required = true) @PathVariable String importId) {
|
||||||
|
requireOperator(request);
|
||||||
return ApiResponse.success(skipPriceAsinService.getDeleteImportProgress(importId));
|
return ApiResponse.success(skipPriceAsinService.getDeleteImportProgress(importId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}/countries/{country}")
|
@PutMapping("/{id}/countries/{country}")
|
||||||
@Operation(summary = "更新指定国家的跳过跟价 ASIN")
|
@Operation(summary = "更新指定国家的跳过跟价 ASIN")
|
||||||
public ApiResponse<SkipPriceAsinItemVo> updateCountry(
|
public ApiResponse<SkipPriceAsinItemVo> updateCountry(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
|
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
|
||||||
@Parameter(description = "国家代码", required = true) @PathVariable String country,
|
@Parameter(description = "国家代码", required = true) @PathVariable String country,
|
||||||
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id") Long operatorId,
|
@Valid @RequestBody SkipPriceAsinCountryUpdateRequest body) {
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Valid @RequestBody SkipPriceAsinCountryUpdateRequest request) {
|
|
||||||
return ApiResponse.success("保存成功", skipPriceAsinService.updateCountry(
|
return ApiResponse.success("保存成功", skipPriceAsinService.updateCountry(
|
||||||
id,
|
id,
|
||||||
country,
|
country,
|
||||||
request.getAsin(),
|
body.getAsin(),
|
||||||
request.getMinimumPrice(),
|
body.getMinimumPrice(),
|
||||||
operatorId,
|
operator.getId(),
|
||||||
Boolean.TRUE.equals(superAdmin)));
|
isSuper(operator)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}/countries/{country}")
|
@DeleteMapping("/{id}/countries/{country}")
|
||||||
@Operation(summary = "删除指定国家的跳过跟价 ASIN")
|
@Operation(summary = "删除指定国家的跳过跟价 ASIN")
|
||||||
public ApiResponse<Void> deleteCountry(
|
public ApiResponse<Void> deleteCountry(
|
||||||
|
HttpServletRequest request,
|
||||||
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
|
@Parameter(description = "主键 ID", required = true) @PathVariable Long id,
|
||||||
@Parameter(description = "国家代码", required = true) @PathVariable String country,
|
@Parameter(description = "国家代码", required = true) @PathVariable String country) {
|
||||||
@Parameter(description = "当前操作人用户 ID") @RequestParam(name = "operator_id") Long operatorId,
|
AdminUserEntity operator = requireOperator(request);
|
||||||
@Parameter(description = "是否超级管理员") @RequestParam(name = "super_admin", defaultValue = "false") Boolean superAdmin) {
|
skipPriceAsinService.deleteCountry(id, country, operator.getId(), isSuper(operator));
|
||||||
skipPriceAsinService.deleteCountry(id, country, operatorId, Boolean.TRUE.equals(superAdmin));
|
|
||||||
return ApiResponse.success("删除成功", null);
|
return ApiResponse.success("删除成功", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isSuper(AdminUserEntity operator) {
|
||||||
|
return "super_admin".equals(adminAuthSupport.currentRole(operator));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package com.nanri.aiimage.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
|
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
||||||
|
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.mock.web.MockFilterChain;
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /api/admin 兜底鉴权过滤器单测:非 admin 路径放行、OPTIONS/豁免前缀放行、
|
||||||
|
* 匿名拒绝返回与全局一致的 401 体、紧急开关可整体关闭。
|
||||||
|
*/
|
||||||
|
class AdminApiGuardFilterTest {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
private AdminApiGuardFilter newFilter(AdminAuthSupport adminAuthSupport, boolean enabled, String exemptPrefixes) {
|
||||||
|
AdminApiGuardFilter filter = new AdminApiGuardFilter(adminAuthSupport, objectMapper);
|
||||||
|
ReflectionTestUtils.setField(filter, "enabled", enabled);
|
||||||
|
ReflectionTestUtils.setField(filter, "exemptPrefixes", exemptPrefixes);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nonAdminApiPathIsNotGuarded() throws Exception {
|
||||||
|
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||||
|
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/admin.html");
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
MockFilterChain chain = new MockFilterChain();
|
||||||
|
|
||||||
|
filter.doFilter(request, response, chain);
|
||||||
|
|
||||||
|
verify(authSupport, never()).requireUserOrInternal(any());
|
||||||
|
assertThat(chain.getRequest()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validIdentityPassesThrough() throws Exception {
|
||||||
|
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||||
|
when(authSupport.requireUserOrInternal(any())).thenReturn(new AdminUserEntity());
|
||||||
|
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/users");
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
MockFilterChain chain = new MockFilterChain();
|
||||||
|
|
||||||
|
filter.doFilter(request, response, chain);
|
||||||
|
|
||||||
|
assertThat(chain.getRequest()).isNotNull();
|
||||||
|
assertThat(response.getContentAsString()).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void anonymousAdminApiRequestRejectedWith401Body() throws Exception {
|
||||||
|
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||||
|
when(authSupport.requireUserOrInternal(any()))
|
||||||
|
.thenThrow(new BusinessException(401, "未登录"));
|
||||||
|
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/shop-keys");
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
MockFilterChain chain = new MockFilterChain();
|
||||||
|
|
||||||
|
filter.doFilter(request, response, chain);
|
||||||
|
|
||||||
|
JsonNode body = objectMapper.readTree(response.getContentAsString());
|
||||||
|
assertThat(body.path("success").asBoolean()).isFalse();
|
||||||
|
assertThat(body.path("code").asInt()).isEqualTo(401);
|
||||||
|
assertThat(chain.getRequest()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void optionsPreflightIsExempt() throws Exception {
|
||||||
|
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||||
|
AdminApiGuardFilter filter = newFilter(authSupport, true, "");
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/api/admin/shop-keys");
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
MockFilterChain chain = new MockFilterChain();
|
||||||
|
|
||||||
|
filter.doFilter(request, response, chain);
|
||||||
|
|
||||||
|
verify(authSupport, never()).requireUserOrInternal(any());
|
||||||
|
assertThat(chain.getRequest()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exemptPrefixPassesThrough() throws Exception {
|
||||||
|
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||||
|
AdminApiGuardFilter filter = newFilter(authSupport, true, "/api/admin/shop-credential-checks");
|
||||||
|
MockHttpServletRequest request =
|
||||||
|
new MockHttpServletRequest("GET", "/api/admin/shop-credential-checks/poll");
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
MockFilterChain chain = new MockFilterChain();
|
||||||
|
|
||||||
|
filter.doFilter(request, response, chain);
|
||||||
|
|
||||||
|
verify(authSupport, never()).requireUserOrInternal(any());
|
||||||
|
assertThat(chain.getRequest()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void disabledGuardPassesEverything() throws Exception {
|
||||||
|
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||||
|
AdminApiGuardFilter filter = newFilter(authSupport, false, "");
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/admin/shop-keys");
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
MockFilterChain chain = new MockFilterChain();
|
||||||
|
|
||||||
|
filter.doFilter(request, response, chain);
|
||||||
|
|
||||||
|
verify(authSupport, never()).requireUserOrInternal(any());
|
||||||
|
assertThat(chain.getRequest()).isNotNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user