From 152ea6eec058dd6756cd8dbb5e4c428a2a83aab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Wed, 2 Sep 2026 16:55:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E7=BA=BF=E5=BA=97=E9=93=BA=E7=AE=A1?= =?UTF-8?q?=E7=90=86-=E6=A3=80=E6=B5=8B=E5=BA=97=E9=93=BA=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E5=8A=9F=E8=83=BD=EF=BC=9A=E5=90=8E=E5=8F=B0=20UI=20?= =?UTF-8?q?=E6=8C=89=E9=92=AE/=E5=BE=BD=E6=A0=87=E3=80=81Python=20?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E8=B7=AF=E7=94=B1=E3=80=81Java=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E4=B8=8E=20service=20=E5=85=A8=E5=A5=97=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=EF=BC=8CV105=20=E8=BF=81=E7=A7=BB=E5=88=A0=E9=99=A4?= =?UTF-8?q?=20biz=5Fshop=5Fcredential=5Fcheck=20=E8=A1=A8=E5=8F=8A?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ShopCredentialCheckController.java | 82 ------- .../mapper/ShopCredentialCheckMapper.java | 9 - .../dto/ShopCredentialCheckCreateRequest.java | 14 -- .../dto/ShopCredentialCheckReportRequest.java | 21 -- .../entity/ShopCredentialCheckEntity.java | 33 --- .../model/vo/ShopCredentialCheckClaimVo.java | 26 -- .../model/vo/ShopCredentialCheckVo.java | 38 --- .../shopkey/model/vo/ShopManageItemVo.java | 2 - .../service/ShopCredentialCheckService.java | 230 ------------------ .../shopkey/service/ShopManageService.java | 50 +--- .../db/V105__drop_shop_credential_check.sql | 2 + .../ShopCredentialCheckServiceTest.java | 147 ----------- .../service/ShopManageServiceTest.java | 3 - backend/blueprints/admin_api.py | 44 ---- backend/static/admin.js | 102 +++----- backend/web_source/admin.html | 40 +-- 16 files changed, 49 insertions(+), 794 deletions(-) delete mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/ShopCredentialCheckController.java delete mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopCredentialCheckMapper.java delete mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopCredentialCheckCreateRequest.java delete mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopCredentialCheckReportRequest.java delete mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopCredentialCheckEntity.java delete mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopCredentialCheckClaimVo.java delete mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopCredentialCheckVo.java delete mode 100644 backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopCredentialCheckService.java create mode 100644 backend-java/src/main/resources/db/V105__drop_shop_credential_check.sql delete mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/ShopCredentialCheckServiceTest.java diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/ShopCredentialCheckController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/ShopCredentialCheckController.java deleted file mode 100644 index d28362bc..00000000 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/ShopCredentialCheckController.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.nanri.aiimage.modules.shopkey.controller; - -import com.nanri.aiimage.common.api.ApiResponse; -import com.nanri.aiimage.modules.shopkey.model.dto.ShopCredentialCheckCreateRequest; -import com.nanri.aiimage.modules.shopkey.model.dto.ShopCredentialCheckReportRequest; -import com.nanri.aiimage.modules.shopkey.model.vo.ShopCredentialCheckClaimVo; -import com.nanri.aiimage.modules.shopkey.model.vo.ShopCredentialCheckVo; -import com.nanri.aiimage.modules.shopkey.service.ShopCredentialCheckService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -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.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -/** - * 店铺密码检测:后台发起任务,在线客户端轮询领取并真实执行,结果回传后台展示。 - * 所有端点均要求 X-Internal-Token(与 /credential 一致),仅供内部自动化调用。 - */ -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/admin/shop-credential-checks") -@Tag(name = "店铺密码检测", description = "后台发起、客户端执行、回传展示") -public class ShopCredentialCheckController { - - @Value("${aiimage.security.internal-token:}") - private String internalToken; - - private final ShopCredentialCheckService shopCredentialCheckService; - - @PostMapping - @Operation(summary = "发起店铺密码检测", description = "同一店铺存在未完成检测时复用已有任务") - public ApiResponse create( - @Valid @RequestBody ShopCredentialCheckCreateRequest request, - @RequestHeader(value = "X-Internal-Token", required = false) String token) { - requireInternalToken(token); - return ApiResponse.success("检测任务已创建", shopCredentialCheckService.create(request.getShopName())); - } - - @GetMapping("/poll") - @Operation(summary = "客户端轮询领取待执行检测任务", description = "无任务返回 data=null") - public ApiResponse poll( - @RequestParam(value = "clientHost", required = false) String clientHost, - @RequestHeader(value = "X-Internal-Token", required = false) String token) { - requireInternalToken(token); - return ApiResponse.success(shopCredentialCheckService.claimForClient(clientHost)); - } - - @PostMapping("/{id}/report") - @Operation(summary = "客户端回传检测结果") - public ApiResponse report( - @Parameter(description = "检测任务 ID", required = true) @PathVariable Long id, - @Valid @RequestBody(required = false) ShopCredentialCheckReportRequest request, - @RequestHeader(value = "X-Internal-Token", required = false) String token) { - requireInternalToken(token); - shopCredentialCheckService.report(id, request); - return ApiResponse.success("检测结果已记录", null); - } - - @GetMapping("/latest") - @Operation(summary = "查询店铺最近一次检测结果", description = "店铺无检测记录时返回 data=null") - public ApiResponse latest( - @RequestParam("shopId") Long shopId, - @RequestHeader(value = "X-Internal-Token", required = false) String token) { - requireInternalToken(token); - return ApiResponse.success(shopCredentialCheckService.latestByShopId(shopId)); - } - - private void requireInternalToken(String token) { - if (internalToken == null || internalToken.isBlank() || token == null || !internalToken.equals(token)) { - throw new com.nanri.aiimage.common.exception.BusinessException("无权访问"); - } - } -} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopCredentialCheckMapper.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopCredentialCheckMapper.java deleted file mode 100644 index 79e31b50..00000000 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopCredentialCheckMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.nanri.aiimage.modules.shopkey.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.nanri.aiimage.modules.shopkey.model.entity.ShopCredentialCheckEntity; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface ShopCredentialCheckMapper extends BaseMapper { -} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopCredentialCheckCreateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopCredentialCheckCreateRequest.java deleted file mode 100644 index 0cf898c1..00000000 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopCredentialCheckCreateRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.nanri.aiimage.modules.shopkey.model.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -@Schema(description = "发起店铺密码检测请求") -public class ShopCredentialCheckCreateRequest { - - @NotBlank(message = "店铺名称不能为空") - @Schema(description = "店铺名称,按 biz_shop_manage.shop_name 定位", example = "美国站-主营") - private String shopName; -} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopCredentialCheckReportRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopCredentialCheckReportRequest.java deleted file mode 100644 index 5e90459e..00000000 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopCredentialCheckReportRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.nanri.aiimage.modules.shopkey.model.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -@Data -@Schema(description = "客户端回传密码检测结果") -public class ShopCredentialCheckReportRequest { - - @Schema(description = "检测结果:SUCCESS=密码正确;FAILED=密码错误;NO_NEED_LOGIN=店铺已登录态(无法直接判定密码);ERROR=打开店铺/执行异常", example = "SUCCESS") - private String status; - - @Schema(description = "结果详情,用于后台展示与排查", example = "账号或密码错误,请重试") - private String detail; - - @Schema(description = "校验失败时的登录接口返回体摘要", example = "{\"error\":\"Incorrect password\"}") - private String raw; - - @Schema(description = "客户端主机标识", example = "PC-2024001") - private String clientHost; -} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopCredentialCheckEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopCredentialCheckEntity.java deleted file mode 100644 index e351f4eb..00000000 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopCredentialCheckEntity.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.nanri.aiimage.modules.shopkey.model.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@TableName("biz_shop_credential_check") -public class ShopCredentialCheckEntity { - - @TableId(type = IdType.AUTO) - private Long id; - @TableField("shop_id") - private Long shopId; - @TableField("shop_name") - private String shopName; - private String status; - private String detail; - @TableField("client_host") - private String clientHost; - @TableField("try_requested_at") - private LocalDateTime tryRequestedAt; - @TableField("check_started_at") - private LocalDateTime checkStartedAt; - @TableField("check_finished_at") - private LocalDateTime checkFinishedAt; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; -} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopCredentialCheckClaimVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopCredentialCheckClaimVo.java deleted file mode 100644 index 201a5ebd..00000000 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopCredentialCheckClaimVo.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.nanri.aiimage.modules.shopkey.model.vo; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.time.LocalDateTime; - -/** - * 客户端轮询领取到的待执行密码检测任务(不包含任何敏感信息)。 - */ -@Data -@Schema(description = "客户端领取的密码检测任务") -public class ShopCredentialCheckClaimVo { - - @Schema(description = "检测任务 ID", example = "31") - private Long id; - - @Schema(description = "店铺名称") - private String shopName; - - @Schema(description = "紫鸟账号(znUsername 为空时客户端用默认)") - private String znUsername; - - @Schema(description = "发起时间") - private LocalDateTime tryRequestedAt; -} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopCredentialCheckVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopCredentialCheckVo.java deleted file mode 100644 index 6f08b55d..00000000 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopCredentialCheckVo.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.nanri.aiimage.modules.shopkey.model.vo; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@Schema(description = "店铺密码检测任务视图") -public class ShopCredentialCheckVo { - - @Schema(description = "检测任务 ID", example = "31") - private Long id; - - @Schema(description = "店铺 ID") - private Long shopId; - - @Schema(description = "店铺名称") - private String shopName; - - @Schema(description = "状态:PENDING/RUNNING/SUCCESS/FAILED/NO_NEED_LOGIN/ERROR") - private String status; - - @Schema(description = "结果详情") - private String detail; - - @Schema(description = "执行客户端标识") - private String clientHost; - - @Schema(description = "发起时间") - private LocalDateTime tryRequestedAt; - - @Schema(description = "执行开始时间") - private LocalDateTime checkStartedAt; - - @Schema(description = "执行完成时间") - private LocalDateTime checkFinishedAt; -} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageItemVo.java index a8f2d5ba..bb37a432 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageItemVo.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageItemVo.java @@ -16,8 +16,6 @@ public class ShopManageItemVo { private String account; private String password; private String passwordMasked; - /** 最近一次密码检测结果视图;从未检测过为 null。 */ - private ShopCredentialCheckVo latestCheck; private LocalDateTime createdAt; private LocalDateTime updatedAt; } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopCredentialCheckService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopCredentialCheckService.java deleted file mode 100644 index f07e839b..00000000 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopCredentialCheckService.java +++ /dev/null @@ -1,230 +0,0 @@ -package com.nanri.aiimage.modules.shopkey.service; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.nanri.aiimage.common.exception.BusinessException; -import com.nanri.aiimage.modules.shopkey.mapper.ShopCredentialCheckMapper; -import com.nanri.aiimage.modules.shopkey.mapper.ShopManageMapper; -import com.nanri.aiimage.modules.shopkey.model.dto.ShopCredentialCheckReportRequest; -import com.nanri.aiimage.modules.shopkey.model.entity.ShopCredentialCheckEntity; -import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageEntity; -import com.nanri.aiimage.modules.shopkey.model.vo.ShopCredentialCheckClaimVo; -import com.nanri.aiimage.modules.shopkey.model.vo.ShopCredentialCheckVo; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.List; - -/** - * 店铺密码检测任务:后台发起 → 在线客户端轮询领取 → 真实打开紫鸟店铺 - * 并尝试登录亚马逊 → 回传结果 → 后台店铺管理页展示。 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class ShopCredentialCheckService { - - public static final String STATUS_PENDING = "PENDING"; - public static final String STATUS_RUNNING = "RUNNING"; - public static final String STATUS_SUCCESS = "SUCCESS"; - public static final String STATUS_FAILED = "FAILED"; - public static final String STATUS_NO_NEED_LOGIN = "NO_NEED_LOGIN"; - public static final String STATUS_ERROR = "ERROR"; - - /** 客户端领取任务时最久保留的 PENDING 老任务(超过则标记过期)。 */ - private static final int PENDING_ACCEPT_MINUTES = 60; - /** RUNNING 执行超时(客户端崩溃/断网),超过则回收为 PENDING 供其他客户端重试。 */ - private static final int RUNNING_STALE_MINUTES = 30; - - private final ShopCredentialCheckMapper shopCredentialCheckMapper; - private final ShopManageMapper shopManageMapper; - - @Transactional - public ShopCredentialCheckVo create(String shopName) { - ShopManageEntity shop = requireShopByName(shopName); - // 同一店铺已有未完成任务(PENDING/RUNNING)时复用,避免重复弹出多个浏览器窗口 - ShopCredentialCheckEntity active = shopCredentialCheckMapper.selectOne(new LambdaQueryWrapper() - .eq(ShopCredentialCheckEntity::getShopId, shop.getId()) - .in(ShopCredentialCheckEntity::getStatus, STATUS_PENDING, STATUS_RUNNING) - .orderByDesc(ShopCredentialCheckEntity::getId) - .last("limit 1")); - if (active != null) { - return toVo(active); - } - ShopCredentialCheckEntity entity = new ShopCredentialCheckEntity(); - entity.setShopId(shop.getId()); - entity.setShopName(shop.getShopName()); - entity.setStatus(STATUS_PENDING); - entity.setTryRequestedAt(LocalDateTime.now()); - shopCredentialCheckMapper.insert(entity); - log.info("[shop-credential-check] created id={} shopId={} shopName={}", entity.getId(), shop.getId(), shop.getShopName()); - return toVo(entity); - } - - /** - * 客户端轮询领取:返回一条 PENDING 任务(跨店铺按 id 升序), - * 并原子置为 RUNNING;无任务时返回 null。 - */ - @Transactional - public ShopCredentialCheckClaimVo claimForClient(String clientHost) { - recycleStaleRunning(); - expireAbandonedPending(); - ShopCredentialCheckEntity pending = shopCredentialCheckMapper.selectOne(new LambdaQueryWrapper() - .eq(ShopCredentialCheckEntity::getStatus, STATUS_PENDING) - .gt(ShopCredentialCheckEntity::getTryRequestedAt, LocalDateTime.now().minusMinutes(PENDING_ACCEPT_MINUTES)) - .orderByAsc(ShopCredentialCheckEntity::getId) - .last("limit 1")); - if (pending == null) { - return null; - } - int updated = shopCredentialCheckMapper.update(null, new LambdaUpdateWrapper() - .eq(ShopCredentialCheckEntity::getId, pending.getId()) - .eq(ShopCredentialCheckEntity::getStatus, STATUS_PENDING) - .set(ShopCredentialCheckEntity::getStatus, STATUS_RUNNING) - .set(ShopCredentialCheckEntity::getCheckStartedAt, LocalDateTime.now()) - .set(ShopCredentialCheckEntity::getClientHost, clientHost)); - if (updated == 0) { - // 被其他客户端抢先领取 - return null; - } - log.info("[shop-credential-check] claimed id={} shopName={} clientHost={}", pending.getId(), pending.getShopName(), clientHost); - ShopCredentialCheckClaimVo vo = new ShopCredentialCheckClaimVo(); - vo.setId(pending.getId()); - vo.setShopName(pending.getShopName()); - vo.setTryRequestedAt(pending.getTryRequestedAt()); - try { - vo.setZnUsername(findZnUsernameByShopName(pending.getShopName())); - } catch (Exception ex) { - log.warn("[shop-credential-check] resolve znUsername failed shopName={} msg={}", pending.getShopName(), ex.getMessage()); - } - return vo; - } - - @Transactional - public void report(Long id, ShopCredentialCheckReportRequest request) { - ShopCredentialCheckEntity entity = getById(id); - if (!STATUS_RUNNING.equals(entity.getStatus())) { - log.warn("[shop-credential-check] ignore stale report id={} currentStatus={}", id, entity.getStatus()); - return; - } - String status = request == null ? null : request.getStatus(); - if (!List.of(STATUS_SUCCESS, STATUS_FAILED, STATUS_NO_NEED_LOGIN, STATUS_ERROR).contains(status)) { - throw new BusinessException("不支持的检测结果状态: " + status); - } - entity.setStatus(status); - entity.setDetail(request.getDetail()); - entity.setClientHost(firstNonBlank(request.getClientHost(), entity.getClientHost())); - entity.setCheckFinishedAt(LocalDateTime.now()); - shopCredentialCheckMapper.updateById(entity); - log.info("[shop-credential-check] reported id={} shopName={} status={} detail={}", - id, entity.getShopName(), status, request.getDetail()); - } - - public ShopCredentialCheckVo latestByShopId(Long shopId) { - if (shopId == null || shopId <= 0) { - return null; - } - ShopCredentialCheckEntity entity = shopCredentialCheckMapper.selectOne(new LambdaQueryWrapper() - .eq(ShopCredentialCheckEntity::getShopId, shopId) - .orderByDesc(ShopCredentialCheckEntity::getId) - .last("limit 1")); - return entity == null ? null : toVo(entity); - } - - private ShopCredentialCheckEntity getById(Long id) { - ShopCredentialCheckEntity entity = shopCredentialCheckMapper.selectById(id); - if (entity == null) { - throw new BusinessException("密码检测任务不存在"); - } - return entity; - } - - private ShopManageEntity requireShopByName(String shopName) { - String normalized = shopName == null ? "" : shopName.trim(); - if (normalized.isEmpty()) { - throw new BusinessException("店铺名称不能为空"); - } - ShopManageEntity entity = shopManageMapper.selectOne(new LambdaQueryWrapper() - .eq(ShopManageEntity::getShopName, normalized) - .last("limit 1")); - if (entity == null) { - throw new BusinessException("后台店铺管理中未找到店铺:" + normalized + ",请先添加店铺信息"); - } - return entity; - } - - private String findZnUsernameByShopName(String shopName) { - String normalized = shopName == null ? "" : shopName.trim(); - if (normalized.isEmpty()) { - return null; - } - ShopManageEntity entity = shopManageMapper.selectOne(new LambdaQueryWrapper() - .select(ShopManageEntity::getZnUsername) - .eq(ShopManageEntity::getShopName, normalized) - .last("limit 1")); - return entity == null ? null : entity.getZnUsername(); - } - - /** - * 客户端执行超时(崩溃/断网)的 RUNNING 回收为 PENDING,供其他在线客户端重试。 - */ - private void recycleStaleRunning() { - List stale = shopCredentialCheckMapper.selectList(new LambdaQueryWrapper() - .eq(ShopCredentialCheckEntity::getStatus, STATUS_RUNNING) - .lt(ShopCredentialCheckEntity::getCheckStartedAt, LocalDateTime.now().minusMinutes(RUNNING_STALE_MINUTES))); - for (ShopCredentialCheckEntity entity : stale) { - int updated = shopCredentialCheckMapper.update(null, new LambdaUpdateWrapper() - .eq(ShopCredentialCheckEntity::getId, entity.getId()) - .eq(ShopCredentialCheckEntity::getStatus, STATUS_RUNNING) - .set(ShopCredentialCheckEntity::getStatus, STATUS_PENDING) - .set(ShopCredentialCheckEntity::getCheckStartedAt, null)); - if (updated > 0) { - log.warn("[shop-credential-check] recycled stale RUNNING id={} shopName={} clientHost={}", - entity.getId(), entity.getShopName(), entity.getClientHost()); - } - } - } - - /** - * 后台发起后长时间无人领取的 PENDING 标记 ERROR;客户端领取入口只捡 60 分钟内新发的, - * 这里只清理历史残留,防止 PENDING 无限堆积。 - */ - private void expireAbandonedPending() { - List abandoned = shopCredentialCheckMapper.selectList(new LambdaQueryWrapper() - .eq(ShopCredentialCheckEntity::getStatus, STATUS_PENDING) - .le(ShopCredentialCheckEntity::getTryRequestedAt, LocalDateTime.now().minusMinutes(PENDING_ACCEPT_MINUTES)) - .last("limit 50")); - for (ShopCredentialCheckEntity entity : abandoned) { - int updated = shopCredentialCheckMapper.update(null, new LambdaUpdateWrapper() - .eq(ShopCredentialCheckEntity::getId, entity.getId()) - .eq(ShopCredentialCheckEntity::getStatus, STATUS_PENDING) - .set(ShopCredentialCheckEntity::getStatus, STATUS_ERROR) - .set(ShopCredentialCheckEntity::getDetail, "超过 " + PENDING_ACCEPT_MINUTES + " 分钟无在线客户端领取,已自动过期") - .set(ShopCredentialCheckEntity::getCheckFinishedAt, LocalDateTime.now())); - if (updated > 0) { - log.warn("[shop-credential-check] expired abandoned PENDING id={} shopName={}", entity.getId(), entity.getShopName()); - } - } - } - - private ShopCredentialCheckVo toVo(ShopCredentialCheckEntity entity) { - ShopCredentialCheckVo vo = new ShopCredentialCheckVo(); - vo.setId(entity.getId()); - vo.setShopId(entity.getShopId()); - vo.setShopName(entity.getShopName()); - vo.setStatus(entity.getStatus()); - vo.setDetail(entity.getDetail()); - vo.setClientHost(entity.getClientHost()); - vo.setTryRequestedAt(entity.getTryRequestedAt()); - vo.setCheckStartedAt(entity.getCheckStartedAt()); - vo.setCheckFinishedAt(entity.getCheckFinishedAt()); - return vo; - } - - private String firstNonBlank(String value, String fallback) { - return value == null || value.isBlank() ? fallback : value; - } -} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java index 79240148..e0d2dbfa 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java @@ -3,14 +3,11 @@ package com.nanri.aiimage.modules.shopkey.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.common.security.ShopCredentialCryptoService; -import com.nanri.aiimage.modules.shopkey.mapper.ShopCredentialCheckMapper; import com.nanri.aiimage.modules.shopkey.mapper.ShopManageMapper; import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageCreateRequest; import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageUpdateRequest; -import com.nanri.aiimage.modules.shopkey.model.entity.ShopCredentialCheckEntity; import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageEntity; import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity; -import com.nanri.aiimage.modules.shopkey.model.vo.ShopCredentialCheckVo; import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageCredentialVo; import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageItemVo; import com.nanri.aiimage.modules.shopkey.model.vo.ShopManagePageVo; @@ -18,7 +15,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -30,7 +26,6 @@ public class ShopManageService { private final ShopManageMapper shopManageMapper; private final ShopManageGroupService shopManageGroupService; private final ShopCredentialCryptoService shopCredentialCryptoService; - private final ShopCredentialCheckMapper shopCredentialCheckMapper; public ShopManagePageVo page(long page, long pageSize, Long groupId, String shopName, Long operatorId, boolean superAdmin) { long safePage = Math.max(page, 1); @@ -74,14 +69,8 @@ public class ShopManageService { .distinct() .toList()); - Map latestCheckByShopId = buildLatestCheckMap(rows); - List items = rows.stream() - .map(entity -> { - ShopManageItemVo vo = toItemVo(entity, groupNameById.get(entity.getGroupId())); - vo.setLatestCheck(latestCheckByShopId.get(entity.getId())); - return vo; - }) + .map(entity -> toItemVo(entity, groupNameById.get(entity.getGroupId()))) .toList(); ShopManagePageVo vo = new ShopManagePageVo(); vo.setItems(items); @@ -199,43 +188,6 @@ public class ShopManageService { return entity; } - /** - * 一次查询本页所有店铺 id 的检测记录(id 倒序),取每个店铺 id 的第一条即最近一次。 - */ - private Map buildLatestCheckMap(List rows) { - List shopIds = rows.stream() - .map(ShopManageEntity::getId) - .filter(id -> id != null && id > 0) - .distinct() - .toList(); - if (shopIds.isEmpty()) { - return Map.of(); - } - List checks = shopCredentialCheckMapper.selectList( - new LambdaQueryWrapper() - .in(ShopCredentialCheckEntity::getShopId, shopIds) - .orderByDesc(ShopCredentialCheckEntity::getId)); - Map map = new LinkedHashMap<>(); - for (ShopCredentialCheckEntity check : checks) { - map.putIfAbsent(check.getShopId(), toCheckVo(check)); - } - return map; - } - - private ShopCredentialCheckVo toCheckVo(ShopCredentialCheckEntity entity) { - ShopCredentialCheckVo vo = new ShopCredentialCheckVo(); - vo.setId(entity.getId()); - vo.setShopId(entity.getShopId()); - vo.setShopName(entity.getShopName()); - vo.setStatus(entity.getStatus()); - vo.setDetail(entity.getDetail()); - vo.setClientHost(entity.getClientHost()); - vo.setTryRequestedAt(entity.getTryRequestedAt()); - vo.setCheckStartedAt(entity.getCheckStartedAt()); - vo.setCheckFinishedAt(entity.getCheckFinishedAt()); - return vo; - } - private void validateGroupAccess(ShopManageEntity entity, Long operatorId, boolean superAdmin) { shopManageGroupService.getAccessibleById(entity.getGroupId(), operatorId, superAdmin); } diff --git a/backend-java/src/main/resources/db/V105__drop_shop_credential_check.sql b/backend-java/src/main/resources/db/V105__drop_shop_credential_check.sql new file mode 100644 index 00000000..ba0a10aa --- /dev/null +++ b/backend-java/src/main/resources/db/V105__drop_shop_credential_check.sql @@ -0,0 +1,2 @@ +-- 下线店铺密码检测功能:后台入口、Python 代理与 Java 接口已随代码移除,历史检测记录一并清理 +DROP TABLE IF EXISTS `biz_shop_credential_check`; diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/ShopCredentialCheckServiceTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/ShopCredentialCheckServiceTest.java deleted file mode 100644 index 88f2b79b..00000000 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/ShopCredentialCheckServiceTest.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.nanri.aiimage.modules.shopkey.service; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; -import com.baomidou.mybatisplus.core.MybatisConfiguration; -import org.apache.ibatis.builder.MapperBuilderAssistant; -import com.nanri.aiimage.common.exception.BusinessException; -import com.nanri.aiimage.modules.shopkey.mapper.ShopCredentialCheckMapper; -import com.nanri.aiimage.modules.shopkey.mapper.ShopManageMapper; -import com.nanri.aiimage.modules.shopkey.model.dto.ShopCredentialCheckReportRequest; -import com.nanri.aiimage.modules.shopkey.model.entity.ShopCredentialCheckEntity; -import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageEntity; -import com.nanri.aiimage.modules.shopkey.model.vo.ShopCredentialCheckClaimVo; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class ShopCredentialCheckServiceTest { - - @Mock - private ShopCredentialCheckMapper shopCredentialCheckMapper; - @Mock - private ShopManageMapper shopManageMapper; - - @InjectMocks - private ShopCredentialCheckService service; - - @BeforeEach - void initTableInfo() { - // MyBatis-Plus Lambda 缓存依赖 TableInfo,单测环境需手动初始化(对应实体) - initTable(ShopCredentialCheckEntity.class); - initTable(ShopManageEntity.class); - } - - private void initTable(Class entityClass) { - MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); - TableInfoHelper.initTableInfo(assistant, entityClass); - } - - @Test - void createReusesActiveTaskForSameShop() { - ShopManageEntity shop = new ShopManageEntity(); - shop.setId(7L); - shop.setShopName("美国站-主营"); - when(shopManageMapper.selectOne(any())).thenReturn(shop); - ShopCredentialCheckEntity active = new ShopCredentialCheckEntity(); - active.setId(3L); - active.setShopId(7L); - active.setShopName("美国站-主营"); - active.setStatus(ShopCredentialCheckService.STATUS_RUNNING); - when(shopCredentialCheckMapper.selectOne(any())).thenReturn(active); - - var vo = service.create("美国站-主营"); - - assertEquals(3L, vo.getId()); - assertEquals("RUNNING", vo.getStatus()); - verify(shopCredentialCheckMapper, never()).insert(any(ShopCredentialCheckEntity.class)); - } - - @Test - void createRejectsUnknownShop() { - when(shopManageMapper.selectOne(any())).thenReturn(null); - - BusinessException ex = assertThrows(BusinessException.class, () -> service.create("不存在店铺")); - assertEquals("后台店铺管理中未找到店铺:不存在店铺,请先添加店铺信息", ex.getMessage()); - } - - @Test - void claimSetsRunningAndReturnsZnUsername() { - ShopCredentialCheckEntity pending = new ShopCredentialCheckEntity(); - pending.setId(9L); - pending.setShopName("店铺-测试"); - when(shopCredentialCheckMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(java.util.List.of()); - when(shopCredentialCheckMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(pending); - when(shopCredentialCheckMapper.update(any(), any(LambdaUpdateWrapper.class))).thenReturn(1); - ShopManageEntity shop = new ShopManageEntity(); - shop.setZnUsername("zn-user-1"); - when(shopManageMapper.selectOne(any())).thenReturn(shop); - - ShopCredentialCheckClaimVo vo = service.claimForClient("PC-01"); - - assertNotNull(vo); - assertEquals(9L, vo.getId()); - assertEquals("店铺-测试", vo.getShopName()); - assertEquals("zn-user-1", vo.getZnUsername()); - verify(shopCredentialCheckMapper, times(1)).update(any(), any(LambdaUpdateWrapper.class)); - } - - @Test - void claimReturnsNullWhenNothingPending() { - when(shopCredentialCheckMapper.selectList(any(LambdaQueryWrapper.class))).thenReturn(java.util.List.of()); - when(shopCredentialCheckMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null); - - assertNull(service.claimForClient("PC-01")); - verify(shopCredentialCheckMapper, never()).update(any(), any(LambdaUpdateWrapper.class)); - } - - @Test - void reportIgnoresStaleStatus() { - ShopCredentialCheckEntity finished = new ShopCredentialCheckEntity(); - finished.setId(5L); - finished.setStatus(ShopCredentialCheckService.STATUS_SUCCESS); - when(shopCredentialCheckMapper.selectById(5L)).thenReturn(finished); - - ShopCredentialCheckReportRequest request = new ShopCredentialCheckReportRequest(); - request.setStatus(ShopCredentialCheckService.STATUS_FAILED); - request.setDetail("密码错误"); - service.report(5L, request); - - verify(shopCredentialCheckMapper, never()).updateById(any(ShopCredentialCheckEntity.class)); - } - - @Test - void reportRecordsFailureDetail() { - ShopCredentialCheckEntity running = new ShopCredentialCheckEntity(); - running.setId(6L); - running.setStatus(ShopCredentialCheckService.STATUS_RUNNING); - when(shopCredentialCheckMapper.selectById(6L)).thenReturn(running); - - ShopCredentialCheckReportRequest request = new ShopCredentialCheckReportRequest(); - request.setStatus(ShopCredentialCheckService.STATUS_FAILED); - request.setDetail("账号或密码错误"); - request.setClientHost("PC-02"); - service.report(6L, request); - - verify(shopCredentialCheckMapper, times(1)).updateById(running); - assertEquals(ShopCredentialCheckService.STATUS_FAILED, running.getStatus()); - assertEquals("账号或密码错误", running.getDetail()); - assertEquals("PC-02", running.getClientHost()); - assertNotNull(running.getCheckFinishedAt()); - } -} diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/ShopManageServiceTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/ShopManageServiceTest.java index b34b5f64..6e6850b6 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/ShopManageServiceTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/shopkey/service/ShopManageServiceTest.java @@ -2,7 +2,6 @@ package com.nanri.aiimage.modules.shopkey.service; import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.common.security.ShopCredentialCryptoService; -import com.nanri.aiimage.modules.shopkey.mapper.ShopCredentialCheckMapper; import com.nanri.aiimage.modules.shopkey.mapper.ShopManageMapper; import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageEntity; import org.junit.jupiter.api.Test; @@ -26,8 +25,6 @@ class ShopManageServiceTest { private ShopManageGroupService shopManageGroupService; @Mock private ShopCredentialCryptoService shopCredentialCryptoService; - @Mock - private ShopCredentialCheckMapper shopCredentialCheckMapper; @InjectMocks private ShopManageService service; diff --git a/backend/blueprints/admin_api.py b/backend/blueprints/admin_api.py index 28fcd32d..6f2f3e26 100644 --- a/backend/blueprints/admin_api.py +++ b/backend/blueprints/admin_api.py @@ -3478,19 +3478,6 @@ def delete_invalid_asin_data(item_id): # ---------- 店铺管理 ---------- def _format_shop_manage_item(item): - latest_check = item.get('latestCheck') - if isinstance(latest_check, dict): - latest_check = { - 'id': latest_check.get('id'), - 'status': latest_check.get('status') or '', - 'detail': latest_check.get('detail') or '', - 'client_host': latest_check.get('clientHost') or '', - 'try_requested_at': (latest_check.get('tryRequestedAt') or '').replace('T', ' ')[:19], - 'check_started_at': (latest_check.get('checkStartedAt') or '').replace('T', ' ')[:19], - 'check_finished_at': (latest_check.get('checkFinishedAt') or '').replace('T', ' ')[:19], - } - else: - latest_check = None return { 'id': item.get('id'), 'group_id': item.get('groupId'), @@ -3500,7 +3487,6 @@ def _format_shop_manage_item(item): 'zn_username': item.get('znUsername') or '', 'account': item.get('account') or '', 'password': item.get('passwordMasked') or '', - 'latest_check': latest_check, 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], } @@ -3580,36 +3566,6 @@ def list_shop_manages(): }) -@admin_api.route('/shop-manage//credential-check', methods=['POST']) -@login_required -def create_shop_credential_check(item_id): - role, current_row, denied = _ensure_backend_menu_access('shop-manage') - if denied: - return denied - - shop_name = (request.args.get('shop_name') or '').strip() - if not shop_name: - return jsonify({'success': False, 'error': '店铺名不能为空'}), 400 - - internal_token = _resolve_internal_token() - if not internal_token: - return jsonify({'success': False, 'error': '内部凭据服务未配置'}), 503 - result, error_response, status = _proxy_backend_java( - 'POST', - '/api/admin/shop-credential-checks', - json_data={'shopName': shop_name}, - headers={'X-Internal-Token': internal_token}, - ) - if error_response is not None: - return error_response, status - check = result.get('data') or {} - return jsonify({ - 'success': True, - 'msg': '检测任务已创建,客户端将在 1 分钟内执行', - 'check': {'id': check.get('id'), 'status': check.get('status') or 'PENDING'}, - }) - - @admin_api.route('/shop-manage//credential') @login_required def get_shop_manage_credential(item_id): diff --git a/backend/static/admin.js b/backend/static/admin.js index 78424e27..ba0af075 100644 --- a/backend/static/admin.js +++ b/backend/static/admin.js @@ -528,6 +528,10 @@ function columnMenuType(item) { return String(item && item.menu_type || 'app').toLowerCase() === 'admin' ? 'admin' : 'app'; } + // 数据层一级分组(不映射真实页面),只用于权限树层级与「上级菜单」候选。 + function isAdminMenuGroup(item) { + return String(item && item.column_key || '').indexOf('admin_group_') === 0; + } function columnDescendantIds(id) { var result = [], pending = [Number(id)]; while (pending.length) { @@ -640,7 +644,7 @@ if (!directIds[id] || item._structureOnly) return; var card = document.createElement('span'); card.className = 'column-permission-card'; - card.textContent = (item.name || '') + ' (' + (item.column_key || '') + ')'; + card.textContent = item.name || '未命名'; var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'col-card-remove'; @@ -740,7 +744,7 @@ renderColumnPermissionWrap(wrapId); }; var text = document.createElement('span'); - text.textContent = (item.name || '') + ' (' + (item.column_key || '') + ')'; + text.textContent = item.name || '未命名'; if (inherited || structureOnly) text.className = 'inherited-label'; label.appendChild(checkbox); label.appendChild(text); @@ -759,7 +763,7 @@ } [ { key: 'admin', label: '后台菜单' }, - { key: 'app', label: 'APP(软件)菜单' } + { key: 'app', label: '软件菜单' } ].forEach(function (group) { var groupItems = allColumnsList.filter(function (item) { return columnMenuType(item) === group.key; @@ -835,13 +839,27 @@ var blockedIds = editingId > 0 ? [editingId].concat(columnDescendantIds(editingId)) : []; select.innerHTML = ''; allColumnsList.filter(function (item) { - return !item._structureOnly && String(item.menu_type || 'app') === String(menuType) && blockedIds.indexOf(columnId(item)) < 0; + // 只有一级分组可以作为上级菜单;分组行可能对管理员不可直接授予(structureOnly), + // 但作为父级仍然合法,因此不做 _structureOnly 过滤。 + return isAdminMenuGroup(item) + && String(item.menu_type || 'app') === String(menuType) + && blockedIds.indexOf(columnId(item)) < 0; }).forEach(function (item) { var option = document.createElement('option'); option.value = item.id; - option.textContent = (item.name || '') + ' (' + (item.column_key || '') + ')'; + option.textContent = item.name || '未命名'; select.appendChild(option); }); + // 兜底:编辑旧菜单时当前父级可能不是分组行,原值保留在选项里避免误改层级。 + if (current && !Array.prototype.some.call(select.options, function (option) { + return option.value === String(current); + })) { + var legacy = allColumnsList.find(function (candidate) { return String(columnId(candidate)) === String(current); }); + var fallback = document.createElement('option'); + fallback.value = current; + fallback.textContent = (legacy && legacy.name ? legacy.name : '原上级菜单') + '(原上级)'; + select.appendChild(fallback); + } select.value = current; }); } @@ -2365,7 +2383,7 @@ items.forEach(function (u) { var opt = document.createElement('option'); opt.value = u.id; - opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')'; + opt.textContent = u.username || ''; sel.appendChild(opt); }); sel.value = cur || ''; @@ -3221,35 +3239,6 @@ shopPasswordIcon(false) + ''; } - function renderShopCheckBadge(check) { - if (!check) return ''; - var map = { - 'SUCCESS': ['ok', '密码正确'], - 'FAILED': ['bad', '密码错误'], - 'RUNNING': ['run', '检测中'], - 'PENDING': ['wait', '等待客户端'], - 'NO_NEED_LOGIN': ['warn', '已登录态'], - 'ERROR': ['bad', '检测异常'] - }; - var entry = map[check.status] || ['wait', check.status || '未知']; - var tipText = [check.status, check.detail, check.check_finished_at].filter(Boolean).join(' · '); - return '
' + escapeHtml(entry[1]) + '
'; - } - - var shopCheckPollTimer = null; - function startShopCheckPolling() { - if (shopCheckPollTimer) return; - var ticks = 0; - shopCheckPollTimer = setInterval(function () { - ticks += 1; - loadShopManage(shopManagePage); - if (ticks >= 9) { - clearInterval(shopCheckPollTimer); - shopCheckPollTimer = null; - } - }, 20000); - } - function renderShopTableText(value, fallback) { var text = String(value == null ? '' : value).trim(); var shown = text || fallback || '-'; @@ -3279,12 +3268,11 @@ '' + renderShopTableText(item.mall_name) + '' + '' + renderShopTableText(item.zn_username) + '' + '' + renderShopTableText(item.account) + '' + - '' + renderShopPasswordCell(item) + renderShopCheckBadge(item.latest_check) + '' + + '' + renderShopPasswordCell(item) + '' + '' + renderShopTableText(item.created_at) + '' + '' + renderShopTableText(item.updated_at) + '' + '' + ' ' + - ' ' + '' + ''; }).join(''); @@ -3365,29 +3353,6 @@ }); }; }); - document.querySelectorAll('[data-shop-credential-check]').forEach(function (btn) { - btn.onclick = function () { - var name = (btn.dataset.shopCheckName || '').replace(/"/g, '"'); - btn.disabled = true; - btn.textContent = '已提交...'; - fetch('/api/admin/shop-manage/' + encodeURIComponent(btn.dataset.shopCredentialCheck) + '/credential-check?shop_name=' + encodeURIComponent(name), { method: 'POST' }) - .then(function (r) { return r.json(); }) - .then(function (res) { - if (res.success) { - alert(res.msg || '检测任务已创建,客户端将在 1 分钟内执行'); - startShopCheckPolling(); - loadShopManage(shopManagePage); - } else { - alert(res.error || '发起检测失败'); - } - }) - .catch(function () { alert('发起检测失败'); }) - .finally(function () { - btn.disabled = false; - btn.textContent = '检测密码'; - }); - }; - }); } function getInvalidAsinDataLockedGroupId() { @@ -5838,6 +5803,7 @@ var COLUMN_PAGE_CATALOG = [ { name: '用户管理', column_key: 'admin_users', route_path: 'users', menu_type: 'admin' }, { name: '菜单权限配置', column_key: 'admin_columns', route_path: 'columns', menu_type: 'admin' }, + { name: '分组管理', column_key: 'admin_group_manage', route_path: 'group-manage', menu_type: 'admin' }, { name: '去重数据汇总', column_key: 'admin_dedupe_total_data', route_path: 'dedupe-total-data', menu_type: 'admin' }, { name: '品牌数据库', column_key: 'admin_invalid_asin_data', route_path: 'invalid-asin-data', menu_type: 'admin' }, { name: '查询ASIN', column_key: 'admin_query_asin', route_path: 'query-asin', menu_type: 'admin' }, @@ -5846,7 +5812,7 @@ { name: '店铺管理', column_key: 'admin_shop_manage', route_path: 'shop-manage', menu_type: 'admin' }, { name: '最低价ASIN设置', column_key: 'admin_skip_price_asin', route_path: 'skip-price-asin', menu_type: 'admin' }, { name: '店铺数据记录', column_key: 'admin_shop_data_crawl_tasks', route_path: 'shop-data-crawl-tasks', menu_type: 'admin' }, - { name: '视频任务管理', column_key: 'admin_image_video_tasks', route_path: 'image-video-tasks', menu_type: 'admin' }, + { name: '视频任务记录', column_key: 'admin_image_video_tasks', route_path: 'image-video-tasks', menu_type: 'admin' }, { name: '生成记录', column_key: 'admin_history', route_path: 'history', menu_type: 'admin' }, { name: '软件版本管理', column_key: 'admin_version', route_path: 'version', menu_type: 'admin' }, { name: '数字人版本管理', column_key: 'digital_human_version', route_path: 'digital-human-version', menu_type: 'admin' }, @@ -5892,14 +5858,17 @@ var options = []; var push = function (item) { var key = columnPageValue(item.column_key, item.route_path); + // 分组行(route_path 为空)不映射真实页面,不出现在页面选择器里。 if (!item.column_key || !item.route_path || seen[key]) return; seen[key] = true; - options.push({ value: key, label: (item.name || item.route_path) + '(' + item.route_path + ')' }); + options.push({ value: key, label: item.name || item.route_path }); }; COLUMN_PAGE_CATALOG.forEach(function (item) { if (String(item.menu_type) === String(menuType)) push(item); }); allColumnsList.forEach(function (item) { + // 一级分组行不是真实页面,不能作为新增菜单的页面来源。 + if (isAdminMenuGroup(item)) return; if (String(item.menu_type || 'app') === String(menuType)) push(item); }); return options; @@ -5921,6 +5890,15 @@ el.textContent = option.label; select.appendChild(el); }); + // 编辑分组行时没有合法页面可选,追加分组虚拟项兜底 + // (用户改不回页面时才需要,保存时按分组处理)。 + if (select.value === '' && current) { + var fallback = document.createElement('option'); + fallback.value = current; + fallback.textContent = '(分组菜单)'; + select.appendChild(fallback); + select.value = current; + } select.value = current; }); } diff --git a/backend/web_source/admin.html b/backend/web_source/admin.html index 75fd8e91..238a2339 100644 --- a/backend/web_source/admin.html +++ b/backend/web_source/admin.html @@ -1044,34 +1044,6 @@ pointer-events: none; } - .shop-check-badge { - display: inline-block; - margin-left: 8px; - padding: 1px 7px; - border-radius: 9px; - font-size: 11px; - line-height: 17px; - white-space: nowrap; - cursor: default; - } - - .shop-check-badge.ok { color: #067647; background: #e6f4ea; border: 1px solid #b7e0c3; } - .shop-check-badge.bad { color: #b42318; background: #fee4e2; border: 1px solid #fecdca; } - .shop-check-badge.run { color: #175cd3; background: #eaf2ff; border: 1px solid #b8d2ff; } - .shop-check-badge.wait { color: #667085; background: #f2f4f7; border: 1px solid #d0d5dd; } - .shop-check-badge.warn { color: #b54708; background: #fef0c7; border: 1px solid #fedf89; } - - .btn-check { - color: #5158d9; - border-color: #c7cbfa; - } - - .btn-check:hover:not(:disabled) { - color: #fff; - background: #5158d9; - border-color: #5158d9; - } - .dedupe-group-access { display: flex; align-items: center; @@ -4851,8 +4823,8 @@
@@ -4861,7 +4833,7 @@

- +
@@ -4886,8 +4858,8 @@
@@ -5556,7 +5528,7 @@ window.__initAdminMenuCollapse(); })(); - +