下线店铺管理-检测店铺密码功能:后台 UI 按钮/徽标、Python 代理路由、Java 接口与 service 全套移除,V105 迁移删除 biz_shop_credential_check 表及历史记录
Build Backend JAR / build (push) Has been cancelled
Build Backend JAR / build (push) Has been cancelled
This commit is contained in:
-82
@@ -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<ShopCredentialCheckVo> 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<ShopCredentialCheckClaimVo> 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<Void> 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<ShopCredentialCheckVo> 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("无权访问");
|
||||
}
|
||||
}
|
||||
}
|
||||
-9
@@ -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<ShopCredentialCheckEntity> {
|
||||
}
|
||||
-14
@@ -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;
|
||||
}
|
||||
-21
@@ -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;
|
||||
}
|
||||
-33
@@ -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;
|
||||
}
|
||||
-26
@@ -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;
|
||||
}
|
||||
-38
@@ -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;
|
||||
}
|
||||
-2
@@ -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;
|
||||
}
|
||||
|
||||
-230
@@ -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<ShopCredentialCheckEntity>()
|
||||
.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<ShopCredentialCheckEntity>()
|
||||
.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<ShopCredentialCheckEntity>()
|
||||
.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<ShopCredentialCheckEntity>()
|
||||
.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<ShopManageEntity>()
|
||||
.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<ShopManageEntity>()
|
||||
.select(ShopManageEntity::getZnUsername)
|
||||
.eq(ShopManageEntity::getShopName, normalized)
|
||||
.last("limit 1"));
|
||||
return entity == null ? null : entity.getZnUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端执行超时(崩溃/断网)的 RUNNING 回收为 PENDING,供其他在线客户端重试。
|
||||
*/
|
||||
private void recycleStaleRunning() {
|
||||
List<ShopCredentialCheckEntity> stale = shopCredentialCheckMapper.selectList(new LambdaQueryWrapper<ShopCredentialCheckEntity>()
|
||||
.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<ShopCredentialCheckEntity>()
|
||||
.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<ShopCredentialCheckEntity> abandoned = shopCredentialCheckMapper.selectList(new LambdaQueryWrapper<ShopCredentialCheckEntity>()
|
||||
.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<ShopCredentialCheckEntity>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
+1
-49
@@ -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<Long, ShopCredentialCheckVo> latestCheckByShopId = buildLatestCheckMap(rows);
|
||||
|
||||
List<ShopManageItemVo> 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<Long, ShopCredentialCheckVo> buildLatestCheckMap(List<ShopManageEntity> rows) {
|
||||
List<Long> shopIds = rows.stream()
|
||||
.map(ShopManageEntity::getId)
|
||||
.filter(id -> id != null && id > 0)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (shopIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
List<ShopCredentialCheckEntity> checks = shopCredentialCheckMapper.selectList(
|
||||
new LambdaQueryWrapper<ShopCredentialCheckEntity>()
|
||||
.in(ShopCredentialCheckEntity::getShopId, shopIds)
|
||||
.orderByDesc(ShopCredentialCheckEntity::getId));
|
||||
Map<Long, ShopCredentialCheckVo> 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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- 下线店铺密码检测功能:后台入口、Python 代理与 Java 接口已随代码移除,历史检测记录一并清理
|
||||
DROP TABLE IF EXISTS `biz_shop_credential_check`;
|
||||
-147
@@ -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());
|
||||
}
|
||||
}
|
||||
-3
@@ -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;
|
||||
|
||||
@@ -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/<int:item_id>/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/<int:item_id>/credential')
|
||||
@login_required
|
||||
def get_shop_manage_credential(item_id):
|
||||
|
||||
+40
-62
@@ -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 = '<option value="">无(一级菜单)</option>';
|
||||
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) + '</button></span>';
|
||||
}
|
||||
|
||||
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 '<div class="shop-check-badge ' + entry[0] + '" title="' + escapeHtml(tipText) + '">' + escapeHtml(entry[1]) + '</div>';
|
||||
}
|
||||
|
||||
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 @@
|
||||
'<td class="shop-col-mall">' + renderShopTableText(item.mall_name) + '</td>' +
|
||||
'<td class="shop-col-automation">' + renderShopTableText(item.zn_username) + '</td>' +
|
||||
'<td class="shop-col-account">' + renderShopTableText(item.account) + '</td>' +
|
||||
'<td class="shop-col-password">' + renderShopPasswordCell(item) + renderShopCheckBadge(item.latest_check) + '</td>' +
|
||||
'<td class="shop-col-password">' + renderShopPasswordCell(item) + '</td>' +
|
||||
'<td class="shop-col-created">' + renderShopTableText(item.created_at) + '</td>' +
|
||||
'<td class="shop-col-updated">' + renderShopTableText(item.updated_at) + '</td>' +
|
||||
'<td class="shop-col-actions">' +
|
||||
'<button type="button" class="btn btn-sm" data-shop-manage-edit="' + escapeHtml(item.id) + '" data-shop-manage="' + (JSON.stringify(item).replace(/"/g, '"')) + '">编辑</button> ' +
|
||||
'<button type="button" class="btn btn-sm btn-check" data-shop-credential-check="' + escapeHtml(item.id) + '" data-shop-check-name="' + escapeHtml(item.shop_name || '') + '">检测密码</button> ' +
|
||||
'<button type="button" class="btn btn-sm btn-danger" data-shop-manage-delete="' + escapeHtml(item.id) + '" data-shop-manage-name="' + escapeHtml(item.shop_name || '') + '">删除</button>' +
|
||||
'</td></tr>';
|
||||
}).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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
<div class="form-group">
|
||||
<label>菜单类型</label>
|
||||
<select id="columnMenuType">
|
||||
<option value="admin">后台(admin)</option>
|
||||
<option value="app">软件(app)</option>
|
||||
<option value="admin">后台</option>
|
||||
<option value="app">软件</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -4861,7 +4833,7 @@
|
||||
</div>
|
||||
<p class="msg" id="msgColumn"></p>
|
||||
<div style="margin-top:16px;display:flex;gap:8px;">
|
||||
<button class="btn" id="btnAddColumn" type="button">新增菜单</button>
|
||||
<button class="btn" id="btnAddColumn" type="button">保存</button>
|
||||
<button class="btn btn-secondary" id="btnCloseCreateColumnModal" type="button">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4886,8 +4858,8 @@
|
||||
<div class="form-group">
|
||||
<label>菜单类型</label>
|
||||
<select id="editColumnMenuType">
|
||||
<option value="admin">后台(admin)</option>
|
||||
<option value="app">软件(app)</option>
|
||||
<option value="admin">后台</option>
|
||||
<option value="app">软件</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -5556,7 +5528,7 @@
|
||||
window.__initAdminMenuCollapse();
|
||||
})();
|
||||
</script>
|
||||
<script src="/static/admin.js?v=asin-create-v4"></script>
|
||||
<script src="/static/admin.js?v=drop-shop-check"></script>
|
||||
<div class="admin-toast-region" id="adminToastRegion" role="status" aria-live="polite" aria-atomic="true"></div>
|
||||
<div class="admin-confirm-mask" id="adminConfirmModal" aria-hidden="true">
|
||||
<section class="admin-confirm-dialog" role="alertdialog" aria-modal="true" aria-labelledby="adminConfirmTitle" aria-describedby="adminConfirmMessage">
|
||||
|
||||
Reference in New Issue
Block a user