提交工作更新
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,19 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
<head>
|
<meta charset="UTF-8" />
|
||||||
<meta charset="UTF-8" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<title>删除品牌 - 数富AI</title>
|
||||||
<title>删除品牌 - 数富AI</title>
|
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-D808cNhy.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-D808cNhy.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-DRn3N4FA.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-DRn3N4FA.js">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-BLoeiUcF.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-BLoeiUcF.css">
|
<link rel="stylesheet" crossorigin href="/assets/delete-brand-DfRLfrtm.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-DfRLfrtm.css">
|
</head>
|
||||||
</head>
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
<body>
|
</body>
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -6,12 +6,19 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class PermissionMenuSchemaInitializer {
|
public class PermissionMenuSchemaInitializer {
|
||||||
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private static final List<DefaultAdminMenu> DEFAULT_ADMIN_MENUS = List.of(
|
||||||
|
new DefaultAdminMenu("数据去重总数据", "admin_dedupe_total_data", "dedupe-total-data"),
|
||||||
|
new DefaultAdminMenu("店铺管理", "admin_shop_manage", "shop-manage"),
|
||||||
|
new DefaultAdminMenu("跳过跟价ASIN", "admin_skip_price_asin", "skip-price-asin")
|
||||||
|
);
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
@@ -27,6 +34,7 @@ public class PermissionMenuSchemaInitializer {
|
|||||||
executeQuietly("ALTER TABLE columns ADD COLUMN menu_type VARCHAR(20) NOT NULL DEFAULT 'app' COMMENT '菜单类型: app/admin' AFTER column_key");
|
executeQuietly("ALTER TABLE columns ADD COLUMN menu_type VARCHAR(20) NOT NULL DEFAULT 'app' COMMENT '菜单类型: app/admin' AFTER column_key");
|
||||||
executeQuietly("ALTER TABLE columns ADD COLUMN route_path VARCHAR(255) NOT NULL DEFAULT '' COMMENT '菜单路由或页面标识' AFTER menu_type");
|
executeQuietly("ALTER TABLE columns ADD COLUMN route_path VARCHAR(255) NOT NULL DEFAULT '' COMMENT '菜单路由或页面标识' AFTER menu_type");
|
||||||
executeQuietly("UPDATE columns SET menu_type = 'app' WHERE menu_type IS NULL OR menu_type = ''");
|
executeQuietly("UPDATE columns SET menu_type = 'app' WHERE menu_type IS NULL OR menu_type = ''");
|
||||||
|
ensureDefaultAdminMenus();
|
||||||
executeQuietly("""
|
executeQuietly("""
|
||||||
INSERT INTO columns (name, column_key, menu_type, route_path)
|
INSERT INTO columns (name, column_key, menu_type, route_path)
|
||||||
SELECT '店铺管理', 'admin_shop_manage', 'admin', 'shop-manage'
|
SELECT '店铺管理', 'admin_shop_manage', 'admin', 'shop-manage'
|
||||||
@@ -45,6 +53,18 @@ public class PermissionMenuSchemaInitializer {
|
|||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureDefaultAdminMenus() {
|
||||||
|
for (DefaultAdminMenu menu : DEFAULT_ADMIN_MENUS) {
|
||||||
|
executeQuietly("""
|
||||||
|
INSERT INTO columns (name, column_key, menu_type, route_path)
|
||||||
|
SELECT '%s', '%s', 'admin', '%s'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM columns WHERE column_key = '%s'
|
||||||
|
)
|
||||||
|
""".formatted(menu.name(), menu.columnKey(), menu.routePath(), menu.columnKey()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void executeQuietly(String sql) {
|
private void executeQuietly(String sql) {
|
||||||
try {
|
try {
|
||||||
jdbcTemplate.execute(sql);
|
jdbcTemplate.execute(sql);
|
||||||
@@ -52,4 +72,7 @@ public class PermissionMenuSchemaInitializer {
|
|||||||
log.debug("[permission-menu] schema init skipped: {}", ex.getMessage());
|
log.debug("[permission-menu] schema init skipped: {}", ex.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record DefaultAdminMenu(String name, String columnKey, String routePath) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.nanri.aiimage.modules.shopkey.controller;
|
||||||
|
|
||||||
|
import com.nanri.aiimage.common.api.ApiResponse;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinItemVo;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinPageVo;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.service.SkipPriceAsinService;
|
||||||
|
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.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/api/admin/skip-price-asins")
|
||||||
|
@Tag(name = "跳过跟价 ASIN", description = "维护店铺按国家跳过跟价的 ASIN")
|
||||||
|
public class SkipPriceAsinController {
|
||||||
|
|
||||||
|
private final SkipPriceAsinService skipPriceAsinService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "分页查询跳过跟价 ASIN")
|
||||||
|
public ApiResponse<SkipPriceAsinPageVo> page(
|
||||||
|
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
|
||||||
|
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize,
|
||||||
|
@Parameter(description = "分组ID") @RequestParam(required = false) Long groupId,
|
||||||
|
@Parameter(description = "店铺名") @RequestParam(required = false) String shopName,
|
||||||
|
@Parameter(description = "ASIN") @RequestParam(required = false) String asin) {
|
||||||
|
return ApiResponse.success(skipPriceAsinService.page(page, pageSize, groupId, shopName, asin));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "新增或覆盖跳过跟价 ASIN")
|
||||||
|
public ApiResponse<SkipPriceAsinItemVo> create(@Valid @RequestBody SkipPriceAsinCreateRequest request) {
|
||||||
|
return ApiResponse.success("保存成功", skipPriceAsinService.createOrUpdate(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}/countries/{country}")
|
||||||
|
@Operation(summary = "删除指定国家的跳过跟价 ASIN")
|
||||||
|
public ApiResponse<Void> deleteCountry(
|
||||||
|
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
|
||||||
|
@Parameter(description = "国家编码", required = true) @PathVariable String country) {
|
||||||
|
skipPriceAsinService.deleteCountry(id, country);
|
||||||
|
return ApiResponse.success("删除成功", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.nanri.aiimage.modules.shopkey.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.entity.SkipPriceAsinEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface SkipPriceAsinMapper extends BaseMapper<SkipPriceAsinEntity> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.nanri.aiimage.modules.shopkey.model.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SkipPriceAsinCreateRequest {
|
||||||
|
|
||||||
|
@NotNull(message = "分组不能为空")
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
@NotBlank(message = "店铺名不能为空")
|
||||||
|
private String shopName;
|
||||||
|
|
||||||
|
@NotEmpty(message = "请至少选择一个国家")
|
||||||
|
private List<String> countries;
|
||||||
|
|
||||||
|
@NotBlank(message = "ASIN 不能为空")
|
||||||
|
private String asin;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
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_skip_price_asin")
|
||||||
|
public class SkipPriceAsinEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@TableField("group_id")
|
||||||
|
private Long groupId;
|
||||||
|
|
||||||
|
@TableField("shop_name")
|
||||||
|
private String shopName;
|
||||||
|
|
||||||
|
@TableField("asin_de")
|
||||||
|
private String asinDe;
|
||||||
|
|
||||||
|
@TableField("asin_uk")
|
||||||
|
private String asinUk;
|
||||||
|
|
||||||
|
@TableField("asin_fr")
|
||||||
|
private String asinFr;
|
||||||
|
|
||||||
|
@TableField("asin_it")
|
||||||
|
private String asinIt;
|
||||||
|
|
||||||
|
@TableField("asin_es")
|
||||||
|
private String asinEs;
|
||||||
|
|
||||||
|
@TableField("created_at")
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@TableField("updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.nanri.aiimage.modules.shopkey.model.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SkipPriceAsinItemVo {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private Long groupId;
|
||||||
|
private String groupName;
|
||||||
|
private String shopName;
|
||||||
|
private String asinDe;
|
||||||
|
private String asinUk;
|
||||||
|
private String asinFr;
|
||||||
|
private String asinIt;
|
||||||
|
private String asinEs;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.nanri.aiimage.modules.shopkey.model.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SkipPriceAsinPageVo {
|
||||||
|
|
||||||
|
private List<SkipPriceAsinItemVo> items = new ArrayList<>();
|
||||||
|
private Long total = 0L;
|
||||||
|
private Long page = 1L;
|
||||||
|
private Long pageSize = 15L;
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
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.modules.shopkey.mapper.SkipPriceAsinMapper;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.dto.SkipPriceAsinCreateRequest;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.entity.SkipPriceAsinEntity;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinItemVo;
|
||||||
|
import com.nanri.aiimage.modules.shopkey.model.vo.SkipPriceAsinPageVo;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SkipPriceAsinService {
|
||||||
|
|
||||||
|
private static final List<String> SUPPORTED_COUNTRIES = List.of("DE", "UK", "FR", "IT", "ES");
|
||||||
|
|
||||||
|
private final SkipPriceAsinMapper skipPriceAsinMapper;
|
||||||
|
private final ShopManageGroupService shopManageGroupService;
|
||||||
|
|
||||||
|
public SkipPriceAsinPageVo page(long page, long pageSize, Long groupId, String shopName, String asin) {
|
||||||
|
long safePage = Math.max(page, 1);
|
||||||
|
long safePageSize = Math.min(Math.max(pageSize, 1), 100);
|
||||||
|
String safeShopName = normalizeBlank(shopName);
|
||||||
|
String safeAsin = normalizeBlank(asin);
|
||||||
|
|
||||||
|
LambdaQueryWrapper<SkipPriceAsinEntity> query = new LambdaQueryWrapper<SkipPriceAsinEntity>()
|
||||||
|
.eq(groupId != null && groupId > 0, SkipPriceAsinEntity::getGroupId, groupId)
|
||||||
|
.like(!safeShopName.isEmpty(), SkipPriceAsinEntity::getShopName, safeShopName)
|
||||||
|
.and(!safeAsin.isEmpty(), wrapper -> wrapper
|
||||||
|
.like(SkipPriceAsinEntity::getAsinDe, safeAsin)
|
||||||
|
.or().like(SkipPriceAsinEntity::getAsinUk, safeAsin)
|
||||||
|
.or().like(SkipPriceAsinEntity::getAsinFr, safeAsin)
|
||||||
|
.or().like(SkipPriceAsinEntity::getAsinIt, safeAsin)
|
||||||
|
.or().like(SkipPriceAsinEntity::getAsinEs, safeAsin))
|
||||||
|
.orderByDesc(SkipPriceAsinEntity::getId);
|
||||||
|
|
||||||
|
Long total = skipPriceAsinMapper.selectCount(query);
|
||||||
|
List<SkipPriceAsinEntity> rows = skipPriceAsinMapper
|
||||||
|
.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize));
|
||||||
|
|
||||||
|
Map<Long, String> groupNameById = buildGroupNameMap(rows.stream()
|
||||||
|
.map(SkipPriceAsinEntity::getGroupId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.distinct()
|
||||||
|
.toList());
|
||||||
|
|
||||||
|
SkipPriceAsinPageVo vo = new SkipPriceAsinPageVo();
|
||||||
|
vo.setItems(rows.stream().map(row -> toItemVo(row, groupNameById.get(row.getGroupId()))).toList());
|
||||||
|
vo.setTotal(total);
|
||||||
|
vo.setPage(safePage);
|
||||||
|
vo.setPageSize(safePageSize);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public SkipPriceAsinItemVo createOrUpdate(SkipPriceAsinCreateRequest request) {
|
||||||
|
ShopManageGroupEntity group = shopManageGroupService.getById(request.getGroupId());
|
||||||
|
String shopName = normalizeRequired(request.getShopName(), "店铺名不能为空");
|
||||||
|
String asin = normalizeAsin(request.getAsin());
|
||||||
|
Set<String> countries = normalizeCountries(request.getCountries());
|
||||||
|
|
||||||
|
SkipPriceAsinEntity entity = skipPriceAsinMapper.selectOne(new LambdaQueryWrapper<SkipPriceAsinEntity>()
|
||||||
|
.eq(SkipPriceAsinEntity::getGroupId, group.getId())
|
||||||
|
.eq(SkipPriceAsinEntity::getShopName, shopName)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
if (entity == null) {
|
||||||
|
entity = new SkipPriceAsinEntity();
|
||||||
|
entity.setGroupId(group.getId());
|
||||||
|
entity.setShopName(shopName);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String country : countries) {
|
||||||
|
setCountryAsin(entity, country, asin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.getId() == null) {
|
||||||
|
skipPriceAsinMapper.insert(entity);
|
||||||
|
} else {
|
||||||
|
skipPriceAsinMapper.updateById(entity);
|
||||||
|
}
|
||||||
|
SkipPriceAsinEntity saved = getById(entity.getId());
|
||||||
|
return toItemVo(saved, group.getGroupName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteCountry(Long id, String country) {
|
||||||
|
SkipPriceAsinEntity entity = getById(id);
|
||||||
|
String normalizedCountry = normalizeCountry(country);
|
||||||
|
setCountryAsin(entity, normalizedCountry, "");
|
||||||
|
if (isEmptyRow(entity)) {
|
||||||
|
skipPriceAsinMapper.deleteById(entity.getId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
skipPriceAsinMapper.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SkipPriceAsinEntity getById(Long id) {
|
||||||
|
SkipPriceAsinEntity entity = skipPriceAsinMapper.selectById(id);
|
||||||
|
if (entity == null) {
|
||||||
|
throw new BusinessException("记录不存在");
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SkipPriceAsinItemVo toItemVo(SkipPriceAsinEntity entity, String groupName) {
|
||||||
|
SkipPriceAsinItemVo vo = new SkipPriceAsinItemVo();
|
||||||
|
vo.setId(entity.getId());
|
||||||
|
vo.setGroupId(entity.getGroupId());
|
||||||
|
vo.setGroupName(groupName == null ? "" : groupName);
|
||||||
|
vo.setShopName(entity.getShopName());
|
||||||
|
vo.setAsinDe(blankToEmpty(entity.getAsinDe()));
|
||||||
|
vo.setAsinUk(blankToEmpty(entity.getAsinUk()));
|
||||||
|
vo.setAsinFr(blankToEmpty(entity.getAsinFr()));
|
||||||
|
vo.setAsinIt(blankToEmpty(entity.getAsinIt()));
|
||||||
|
vo.setAsinEs(blankToEmpty(entity.getAsinEs()));
|
||||||
|
vo.setCreatedAt(entity.getCreatedAt());
|
||||||
|
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, String> buildGroupNameMap(List<Long> groupIds) {
|
||||||
|
if (groupIds == null || groupIds.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
LinkedHashMap<Long, String> map = new LinkedHashMap<>();
|
||||||
|
for (Long groupId : groupIds) {
|
||||||
|
try {
|
||||||
|
ShopManageGroupEntity group = shopManageGroupService.getById(groupId);
|
||||||
|
map.put(groupId, group.getGroupName());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
map.put(groupId, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> normalizeCountries(List<String> countries) {
|
||||||
|
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||||
|
if (countries != null) {
|
||||||
|
for (String country : countries) {
|
||||||
|
normalized.add(normalizeCountry(country));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
throw new BusinessException("请至少选择一个国家");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeCountry(String country) {
|
||||||
|
String normalized = normalizeBlank(country).toUpperCase(Locale.ROOT);
|
||||||
|
if (!SUPPORTED_COUNTRIES.contains(normalized)) {
|
||||||
|
throw new BusinessException("国家参数不支持: " + country);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeAsin(String asin) {
|
||||||
|
String normalized = normalizeRequired(asin, "ASIN 不能为空").toUpperCase(Locale.ROOT);
|
||||||
|
if (normalized.length() > 64) {
|
||||||
|
throw new BusinessException("ASIN 长度不能超过 64");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRequired(String value, String message) {
|
||||||
|
String normalized = normalizeBlank(value);
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
throw new BusinessException(message);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeBlank(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String blankToEmpty(String value) {
|
||||||
|
return value == null ? "" : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCountryAsin(SkipPriceAsinEntity entity, String country, String asin) {
|
||||||
|
String value = normalizeBlank(asin);
|
||||||
|
switch (country) {
|
||||||
|
case "DE" -> entity.setAsinDe(value);
|
||||||
|
case "UK" -> entity.setAsinUk(value);
|
||||||
|
case "FR" -> entity.setAsinFr(value);
|
||||||
|
case "IT" -> entity.setAsinIt(value);
|
||||||
|
case "ES" -> entity.setAsinEs(value);
|
||||||
|
default -> throw new BusinessException("国家参数不支持: " + country);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEmptyRow(SkipPriceAsinEntity entity) {
|
||||||
|
return normalizeBlank(entity.getAsinDe()).isEmpty()
|
||||||
|
&& normalizeBlank(entity.getAsinUk()).isEmpty()
|
||||||
|
&& normalizeBlank(entity.getAsinFr()).isEmpty()
|
||||||
|
&& normalizeBlank(entity.getAsinIt()).isEmpty()
|
||||||
|
&& normalizeBlank(entity.getAsinEs()).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS biz_skip_price_asin (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
|
||||||
|
group_id BIGINT NOT NULL COMMENT '分组ID',
|
||||||
|
shop_name VARCHAR(128) NOT NULL COMMENT '店铺名',
|
||||||
|
asin_de VARCHAR(64) NOT NULL DEFAULT '' COMMENT '德国 ASIN',
|
||||||
|
asin_uk VARCHAR(64) NOT NULL DEFAULT '' COMMENT '英国 ASIN',
|
||||||
|
asin_fr VARCHAR(64) NOT NULL DEFAULT '' COMMENT '法国 ASIN',
|
||||||
|
asin_it VARCHAR(64) NOT NULL DEFAULT '' COMMENT '意大利 ASIN',
|
||||||
|
asin_es VARCHAR(64) NOT NULL DEFAULT '' COMMENT '西班牙 ASIN',
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
UNIQUE KEY uk_group_shop_name (group_id, shop_name),
|
||||||
|
KEY idx_shop_name (shop_name),
|
||||||
|
KEY idx_created_at (created_at)
|
||||||
|
) COMMENT='跳过跟价 ASIN';
|
||||||
|
|
||||||
|
INSERT INTO `columns` (`name`, `column_key`, `menu_type`, `route_path`)
|
||||||
|
SELECT '数据去重总数据', 'admin_dedupe_total_data', 'admin', 'dedupe-total-data'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `columns` WHERE `column_key` = 'admin_dedupe_total_data'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `columns` (`name`, `column_key`, `menu_type`, `route_path`)
|
||||||
|
SELECT '店铺管理', 'admin_shop_manage', 'admin', 'shop-manage'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `columns` WHERE `column_key` = 'admin_shop_manage'
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `columns` (`name`, `column_key`, `menu_type`, `route_path`)
|
||||||
|
SELECT '跳过跟价ASIN', 'admin_skip_price_asin', 'admin', 'skip-price-asin'
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `columns` WHERE `column_key` = 'admin_skip_price_asin'
|
||||||
|
);
|
||||||
Binary file not shown.
@@ -19,6 +19,8 @@ ALLOWED_DEDUPE_TOTAL_DATA_ADMIN_USERNAMES = {'刘丽泓'}
|
|||||||
|
|
||||||
|
|
||||||
def _can_access_dedupe_total_data(role, current_row):
|
def _can_access_dedupe_total_data(role, current_row):
|
||||||
|
_, _, denied = _ensure_admin_menu_access('dedupe-total-data')
|
||||||
|
return denied is None
|
||||||
if role == 'super_admin':
|
if role == 'super_admin':
|
||||||
return True
|
return True
|
||||||
if role != 'admin' or not current_row:
|
if role != 'admin' or not current_row:
|
||||||
@@ -27,6 +29,7 @@ def _can_access_dedupe_total_data(role, current_row):
|
|||||||
|
|
||||||
|
|
||||||
def _ensure_dedupe_total_data_access():
|
def _ensure_dedupe_total_data_access():
|
||||||
|
return _ensure_admin_menu_access('dedupe-total-data')
|
||||||
role, current_row = get_current_admin_role()
|
role, current_row = get_current_admin_role()
|
||||||
if not _can_access_dedupe_total_data(role, current_row):
|
if not _can_access_dedupe_total_data(role, current_row):
|
||||||
return None, None, (jsonify({'success': False, 'error': '无权限访问数据去重总数据'}), 403)
|
return None, None, (jsonify({'success': False, 'error': '无权限访问数据去重总数据'}), 403)
|
||||||
@@ -41,6 +44,24 @@ except ImportError:
|
|||||||
|
|
||||||
admin_api = Blueprint('admin_api', __name__, url_prefix='/api/admin')
|
admin_api = Blueprint('admin_api', __name__, url_prefix='/api/admin')
|
||||||
|
|
||||||
|
ADMIN_MENU_ACCESS_CONFIG = {
|
||||||
|
'dedupe-total-data': {
|
||||||
|
'column_key': 'admin_dedupe_total_data',
|
||||||
|
'route_path': 'dedupe-total-data',
|
||||||
|
'error': '鏃犳潈闄愯闂暟鎹幓閲嶆€绘暟鎹?',
|
||||||
|
},
|
||||||
|
'shop-manage': {
|
||||||
|
'column_key': 'admin_shop_manage',
|
||||||
|
'route_path': 'shop-manage',
|
||||||
|
'error': '鏃犳潈闄愯闂簵閾虹鐞嗘ā鍧?',
|
||||||
|
},
|
||||||
|
'skip-price-asin': {
|
||||||
|
'column_key': 'admin_skip_price_asin',
|
||||||
|
'route_path': 'skip-price-asin',
|
||||||
|
'error': '鏃犳潈闄愯闂烦杩囪窡浠稟SIN 妯″潡',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _safe_version_key(version):
|
def _safe_version_key(version):
|
||||||
"""将版本号转为安全的 OSS 对象名部分"""
|
"""将版本号转为安全的 OSS 对象名部分"""
|
||||||
@@ -90,6 +111,47 @@ def _sync_user_column_permissions(user_id, column_ids):
|
|||||||
return error_response, status
|
return error_response, status
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_admin_permission_sets():
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return set(), set()
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT c.column_key, c.route_path
|
||||||
|
FROM user_column_permission ucp
|
||||||
|
INNER JOIN columns c ON c.id = ucp.column_id
|
||||||
|
WHERE ucp.user_id = %s AND c.menu_type = 'admin'""",
|
||||||
|
(user_id,),
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
key_set = {(row.get('column_key') or '').strip() for row in rows if (row.get('column_key') or '').strip()}
|
||||||
|
route_set = {(row.get('route_path') or '').strip() for row in rows if (row.get('route_path') or '').strip()}
|
||||||
|
return key_set, route_set
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_admin_menu_access(*menu_names):
|
||||||
|
role, current_row = get_current_admin_role()
|
||||||
|
if role == 'super_admin':
|
||||||
|
return role, current_row, None
|
||||||
|
if role != 'admin' or not current_row:
|
||||||
|
return role, current_row, (jsonify({'success': False, 'error': '闇€瑕佺鐞嗗憳鏉冮檺'}), 403)
|
||||||
|
key_set, route_set = _get_current_admin_permission_sets()
|
||||||
|
for menu_name in menu_names:
|
||||||
|
config = ADMIN_MENU_ACCESS_CONFIG.get(menu_name) or {}
|
||||||
|
column_key = (config.get('column_key') or '').strip()
|
||||||
|
route_path = (config.get('route_path') or '').strip()
|
||||||
|
if (column_key and column_key in key_set) or (route_path and route_path in route_set):
|
||||||
|
return role, current_row, None
|
||||||
|
fallback_menu = menu_names[0] if menu_names else ''
|
||||||
|
fallback_config = ADMIN_MENU_ACCESS_CONFIG.get(fallback_menu) or {}
|
||||||
|
error_message = fallback_config.get('error') or '鏃犳潈闄愯闂綋鍓嶆ā鍧?'
|
||||||
|
return role, current_row, (jsonify({'success': False, 'error': error_message}), 403)
|
||||||
|
|
||||||
|
|
||||||
# ---------- 用户管理 ----------
|
# ---------- 用户管理 ----------
|
||||||
|
|
||||||
@admin_api.route('/users')
|
@admin_api.route('/users')
|
||||||
@@ -936,6 +998,9 @@ def delete_dedupe_total_data(item_id):
|
|||||||
@admin_api.route('/shop-manages')
|
@admin_api.route('/shop-manages')
|
||||||
@admin_required
|
@admin_required
|
||||||
def list_shop_manages():
|
def list_shop_manages():
|
||||||
|
_, _, denied = _ensure_admin_menu_access('shop-manage')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
page = max(1, int(request.args.get('page', 1)))
|
page = max(1, int(request.args.get('page', 1)))
|
||||||
page_size = min(100, max(1, int(request.args.get('page_size', 15))))
|
page_size = min(100, max(1, int(request.args.get('page_size', 15))))
|
||||||
group_id_raw = (request.args.get('group_id') or '').strip()
|
group_id_raw = (request.args.get('group_id') or '').strip()
|
||||||
@@ -986,6 +1051,9 @@ def list_shop_manages():
|
|||||||
@admin_api.route('/shop-manage', methods=['POST'])
|
@admin_api.route('/shop-manage', methods=['POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def create_shop_manage():
|
def create_shop_manage():
|
||||||
|
_, _, denied = _ensure_admin_menu_access('shop-manage')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
payload = {
|
payload = {
|
||||||
'groupId': data.get('group_id'),
|
'groupId': data.get('group_id'),
|
||||||
@@ -1022,6 +1090,9 @@ def create_shop_manage():
|
|||||||
@admin_api.route('/shop-manage/<int:item_id>', methods=['PUT'])
|
@admin_api.route('/shop-manage/<int:item_id>', methods=['PUT'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_shop_manage(item_id):
|
def update_shop_manage(item_id):
|
||||||
|
_, _, denied = _ensure_admin_menu_access('shop-manage')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
payload = {
|
payload = {
|
||||||
'groupId': data.get('group_id'),
|
'groupId': data.get('group_id'),
|
||||||
@@ -1057,6 +1128,9 @@ def update_shop_manage(item_id):
|
|||||||
@admin_api.route('/shop-manage/<int:item_id>', methods=['DELETE'])
|
@admin_api.route('/shop-manage/<int:item_id>', methods=['DELETE'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_shop_manage(item_id):
|
def delete_shop_manage(item_id):
|
||||||
|
_, _, denied = _ensure_admin_menu_access('shop-manage')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
result, error_response, status = _proxy_backend_java(
|
result, error_response, status = _proxy_backend_java(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
f'/api/admin/shop-manages/{item_id}',
|
f'/api/admin/shop-manages/{item_id}',
|
||||||
@@ -1072,6 +1146,9 @@ def delete_shop_manage(item_id):
|
|||||||
@admin_api.route('/shop-manage-groups')
|
@admin_api.route('/shop-manage-groups')
|
||||||
@admin_required
|
@admin_required
|
||||||
def list_shop_manage_groups():
|
def list_shop_manage_groups():
|
||||||
|
_, _, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
result, error_response, status = _proxy_backend_java(
|
result, error_response, status = _proxy_backend_java(
|
||||||
'GET',
|
'GET',
|
||||||
'/api/admin/shop-manages/groups',
|
'/api/admin/shop-manages/groups',
|
||||||
@@ -1093,6 +1170,9 @@ def list_shop_manage_groups():
|
|||||||
@admin_api.route('/shop-manage-group', methods=['POST'])
|
@admin_api.route('/shop-manage-group', methods=['POST'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def create_shop_manage_group():
|
def create_shop_manage_group():
|
||||||
|
_, _, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
payload = {'groupName': (data.get('group_name') or '').strip()}
|
payload = {'groupName': (data.get('group_name') or '').strip()}
|
||||||
result, error_response, status = _proxy_backend_java(
|
result, error_response, status = _proxy_backend_java(
|
||||||
@@ -1118,6 +1198,9 @@ def create_shop_manage_group():
|
|||||||
@admin_api.route('/shop-manage-group/<int:item_id>', methods=['PUT'])
|
@admin_api.route('/shop-manage-group/<int:item_id>', methods=['PUT'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def update_shop_manage_group(item_id):
|
def update_shop_manage_group(item_id):
|
||||||
|
_, _, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
data = request.get_json() or {}
|
data = request.get_json() or {}
|
||||||
payload = {'groupName': (data.get('group_name') or '').strip()}
|
payload = {'groupName': (data.get('group_name') or '').strip()}
|
||||||
result, error_response, status = _proxy_backend_java(
|
result, error_response, status = _proxy_backend_java(
|
||||||
@@ -1140,9 +1223,123 @@ def update_shop_manage_group(item_id):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_api.route('/skip-price-asins')
|
||||||
|
@admin_required
|
||||||
|
def list_skip_price_asins():
|
||||||
|
_, _, denied = _ensure_admin_menu_access('skip-price-asin')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
|
page = max(1, int(request.args.get('page', 1)))
|
||||||
|
page_size = min(100, max(1, int(request.args.get('page_size', 15))))
|
||||||
|
group_id_raw = (request.args.get('group_id') or '').strip()
|
||||||
|
shop_name = (request.args.get('shop_name') or '').strip()
|
||||||
|
asin = (request.args.get('asin') or '').strip()
|
||||||
|
params = {'page': page, 'pageSize': page_size}
|
||||||
|
if group_id_raw:
|
||||||
|
try:
|
||||||
|
group_id = int(group_id_raw)
|
||||||
|
if group_id > 0:
|
||||||
|
params['groupId'] = group_id
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if shop_name:
|
||||||
|
params['shopName'] = shop_name
|
||||||
|
if asin:
|
||||||
|
params['asin'] = asin
|
||||||
|
result, error_response, status = _proxy_backend_java(
|
||||||
|
'GET',
|
||||||
|
'/api/admin/skip-price-asins',
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
if error_response is not None:
|
||||||
|
return error_response, status
|
||||||
|
payload = result.get('data') or {}
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
'id': item.get('id'),
|
||||||
|
'group_id': item.get('groupId'),
|
||||||
|
'group_name': item.get('groupName') or '',
|
||||||
|
'shop_name': item.get('shopName') or '',
|
||||||
|
'asin_de': item.get('asinDe') or '',
|
||||||
|
'asin_uk': item.get('asinUk') or '',
|
||||||
|
'asin_fr': item.get('asinFr') or '',
|
||||||
|
'asin_it': item.get('asinIt') or '',
|
||||||
|
'asin_es': item.get('asinEs') or '',
|
||||||
|
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||||
|
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||||||
|
}
|
||||||
|
for item in (payload.get('items') or [])
|
||||||
|
]
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'items': items,
|
||||||
|
'total': payload.get('total') or 0,
|
||||||
|
'page': payload.get('page') or page,
|
||||||
|
'page_size': payload.get('pageSize') or page_size,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_api.route('/skip-price-asin', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def create_skip_price_asin():
|
||||||
|
_, _, denied = _ensure_admin_menu_access('skip-price-asin')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
|
data = request.get_json() or {}
|
||||||
|
payload = {
|
||||||
|
'groupId': data.get('group_id'),
|
||||||
|
'shopName': (data.get('shop_name') or '').strip(),
|
||||||
|
'countries': data.get('countries') or [],
|
||||||
|
'asin': (data.get('asin') or '').strip(),
|
||||||
|
}
|
||||||
|
result, error_response, status = _proxy_backend_java(
|
||||||
|
'POST',
|
||||||
|
'/api/admin/skip-price-asins',
|
||||||
|
json_data=payload,
|
||||||
|
)
|
||||||
|
if error_response is not None:
|
||||||
|
return error_response, status
|
||||||
|
item = result.get('data') or {}
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'msg': result.get('message') or '淇濆瓨鎴愬姛',
|
||||||
|
'item': {
|
||||||
|
'id': item.get('id'),
|
||||||
|
'group_id': item.get('groupId'),
|
||||||
|
'group_name': item.get('groupName') or '',
|
||||||
|
'shop_name': item.get('shopName') or '',
|
||||||
|
'asin_de': item.get('asinDe') or '',
|
||||||
|
'asin_uk': item.get('asinUk') or '',
|
||||||
|
'asin_fr': item.get('asinFr') or '',
|
||||||
|
'asin_it': item.get('asinIt') or '',
|
||||||
|
'asin_es': item.get('asinEs') or '',
|
||||||
|
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||||
|
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@admin_api.route('/skip-price-asin/<int:item_id>/country/<country>', methods=['DELETE'])
|
||||||
|
@admin_required
|
||||||
|
def delete_skip_price_asin_country(item_id, country):
|
||||||
|
_, _, denied = _ensure_admin_menu_access('skip-price-asin')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
|
result, error_response, status = _proxy_backend_java(
|
||||||
|
'DELETE',
|
||||||
|
f'/api/admin/skip-price-asins/{item_id}/countries/{country}',
|
||||||
|
)
|
||||||
|
if error_response is not None:
|
||||||
|
return error_response, status
|
||||||
|
return jsonify({'success': True, 'msg': result.get('message') or '鍒犻櫎鎴愬姛'})
|
||||||
|
|
||||||
|
|
||||||
@admin_api.route('/shop-manage-group/<int:item_id>', methods=['DELETE'])
|
@admin_api.route('/shop-manage-group/<int:item_id>', methods=['DELETE'])
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_shop_manage_group(item_id):
|
def delete_shop_manage_group(item_id):
|
||||||
|
_, _, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
result, error_response, status = _proxy_backend_java(
|
result, error_response, status = _proxy_backend_java(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
f'/api/admin/shop-manages/groups/{item_id}',
|
f'/api/admin/shop-manages/groups/{item_id}',
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
<div class="tab" data-tab="dedupe-total-data">数据去重总数据</div>
|
<div class="tab" data-tab="dedupe-total-data">数据去重总数据</div>
|
||||||
<div class="tab" data-tab="shop-keys">店铺密钥管理</div>
|
<div class="tab" data-tab="shop-keys">店铺密钥管理</div>
|
||||||
<div class="tab" data-tab="shop-manage">店铺管理</div>
|
<div class="tab" data-tab="shop-manage">店铺管理</div>
|
||||||
|
<div class="tab" data-tab="skip-price-asin">跳过跟价 ASIN</div>
|
||||||
<div class="tab" data-tab="history">查看生成记录</div>
|
<div class="tab" data-tab="history">查看生成记录</div>
|
||||||
<div class="tab" data-tab="version">版本管理</div>
|
<div class="tab" data-tab="version">版本管理</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -421,6 +422,79 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 查看生成记录 -->
|
<!-- 查看生成记录 -->
|
||||||
|
<div id="panel-skip-price-asin" class="tab-panel">
|
||||||
|
<div class="form-box">
|
||||||
|
<h3 style="margin-bottom:16px;font-size:15px;">新增 ASIN</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group" style="min-width:220px;">
|
||||||
|
<label>分组</label>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;">
|
||||||
|
<select id="skipPriceAsinGroupSelect" style="min-width:150px;">
|
||||||
|
<option value="">请选择分组</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-secondary" id="btnManageSkipPriceAsinGroups" type="button">管理分组</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="min-width:180px;">
|
||||||
|
<label>店铺名</label>
|
||||||
|
<input type="text" id="skipPriceAsinShopName" placeholder="请输入店铺名称">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="min-width:220px;">
|
||||||
|
<label>国家</label>
|
||||||
|
<select id="skipPriceAsinCountries" multiple size="5">
|
||||||
|
<option value="DE">德国</option>
|
||||||
|
<option value="UK">英国</option>
|
||||||
|
<option value="FR">法国</option>
|
||||||
|
<option value="IT">意大利</option>
|
||||||
|
<option value="ES">西班牙</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="min-width:180px;">
|
||||||
|
<label>ASIN</label>
|
||||||
|
<input type="text" id="skipPriceAsinValue" placeholder="请输入 ASIN">
|
||||||
|
</div>
|
||||||
|
<button class="btn" id="btnCreateSkipPriceAsin">新增 ASIN</button>
|
||||||
|
</div>
|
||||||
|
<p class="msg" id="msgSkipPriceAsin"></p>
|
||||||
|
</div>
|
||||||
|
<div class="panel-box">
|
||||||
|
<h3 style="margin-bottom:16px;font-size:15px;">店铺列表</h3>
|
||||||
|
<div class="form-row" style="margin-bottom:12px;">
|
||||||
|
<div class="form-group" style="min-width:180px;">
|
||||||
|
<label>分组</label>
|
||||||
|
<select id="skipPriceAsinFilterGroupId">
|
||||||
|
<option value="">全部分组</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="min-width:220px;">
|
||||||
|
<label>店铺名</label>
|
||||||
|
<input type="text" id="skipPriceAsinFilterShopName" placeholder="请输入店铺名">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="min-width:220px;">
|
||||||
|
<label>ASIN</label>
|
||||||
|
<input type="text" id="skipPriceAsinFilterAsin" placeholder="请输入 ASIN">
|
||||||
|
</div>
|
||||||
|
<button class="btn" id="btnSearchSkipPriceAsin">查询</button>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>序号</th>
|
||||||
|
<th>分组</th>
|
||||||
|
<th>店铺名</th>
|
||||||
|
<th>德国</th>
|
||||||
|
<th>英国</th>
|
||||||
|
<th>法国</th>
|
||||||
|
<th>意大利</th>
|
||||||
|
<th>西班牙</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="skipPriceAsinListBody"></tbody>
|
||||||
|
</table>
|
||||||
|
<div class="pagination" id="skipPriceAsinPagination"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="panel-history" class="tab-panel">
|
<div id="panel-history" class="tab-panel">
|
||||||
<div class="form-box">
|
<div class="form-box">
|
||||||
<h3 style="margin-bottom:16px;font-size:15px;">筛选条件</h3>
|
<h3 style="margin-bottom:16px;font-size:15px;">筛选条件</h3>
|
||||||
@@ -627,6 +701,23 @@
|
|||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
// Tab 切换
|
// Tab 切换
|
||||||
|
var ADMIN_TAB_ACCESS_CONFIG = {
|
||||||
|
'columns': { superAdminOnly: true },
|
||||||
|
'shop-keys': { superAdminOnly: true },
|
||||||
|
'dedupe-total-data': { columnKey: 'admin_dedupe_total_data', routePath: 'dedupe-total-data' },
|
||||||
|
'shop-manage': { columnKey: 'admin_shop_manage', routePath: 'shop-manage' },
|
||||||
|
'skip-price-asin': { columnKey: 'admin_skip_price_asin', routePath: 'skip-price-asin' }
|
||||||
|
};
|
||||||
|
function runTabLoader(tabName) {
|
||||||
|
if (tabName === 'users') { loadUsers(1); loadColumnsForPermission(); }
|
||||||
|
else if (tabName === 'columns') loadColumns();
|
||||||
|
else if (tabName === 'dedupe-total-data') loadDedupeTotalData(1);
|
||||||
|
else if (tabName === 'shop-keys') loadShopKeys(1);
|
||||||
|
else if (tabName === 'shop-manage') loadShopManage(1);
|
||||||
|
else if (tabName === 'skip-price-asin') loadSkipPriceAsin(1);
|
||||||
|
else if (tabName === 'history') loadHistory(1);
|
||||||
|
else if (tabName === 'version') loadVersions();
|
||||||
|
}
|
||||||
document.querySelectorAll('.tab').forEach(function(t) {
|
document.querySelectorAll('.tab').forEach(function(t) {
|
||||||
t.onclick = function() {
|
t.onclick = function() {
|
||||||
document.querySelectorAll('.tab').forEach(function(x) { x.classList.remove('active'); });
|
document.querySelectorAll('.tab').forEach(function(x) { x.classList.remove('active'); });
|
||||||
@@ -634,13 +725,7 @@
|
|||||||
t.classList.add('active');
|
t.classList.add('active');
|
||||||
var id = 'panel-' + t.dataset.tab;
|
var id = 'panel-' + t.dataset.tab;
|
||||||
document.getElementById(id).classList.add('active');
|
document.getElementById(id).classList.add('active');
|
||||||
if (t.dataset.tab === 'users') { loadUsers(1); loadColumnsForPermission(); }
|
runTabLoader(t.dataset.tab);
|
||||||
else if (t.dataset.tab === 'columns') loadColumns();
|
|
||||||
else if (t.dataset.tab === 'dedupe-total-data') loadDedupeTotalData(1);
|
|
||||||
else if (t.dataset.tab === 'shop-keys') loadShopKeys(1);
|
|
||||||
else if (t.dataset.tab === 'shop-manage') loadShopManage(1);
|
|
||||||
else if (t.dataset.tab === 'history') loadHistory(1);
|
|
||||||
else if (t.dataset.tab === 'version') loadVersions();
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -695,7 +780,7 @@
|
|||||||
bindUserActions();
|
bindUserActions();
|
||||||
updateCreateFormByRole();
|
updateCreateFormByRole();
|
||||||
updateUserFilterByRole();
|
updateUserFilterByRole();
|
||||||
updateDedupeTotalDataAccess();
|
refreshAdminTabAccess();
|
||||||
loadCurrentUserAdminPermissions();
|
loadCurrentUserAdminPermissions();
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
@@ -794,39 +879,31 @@
|
|||||||
if (usersPanel) usersPanel.classList.add('active');
|
if (usersPanel) usersPanel.classList.add('active');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function updateAdminOnlyAccess() {
|
function hasAdminTabAccess(tabName) {
|
||||||
var canUse = currentUserRole === 'super_admin';
|
var config = ADMIN_TAB_ACCESS_CONFIG[tabName];
|
||||||
applyTabAccess('columns', canUse);
|
if (!config) return true;
|
||||||
applyTabAccess('shop-keys', canUse);
|
if (currentUserRole === 'super_admin') return true;
|
||||||
|
if (config.superAdminOnly) return false;
|
||||||
|
return !!currentUserAdminPermissionKeys[config.columnKey] ||
|
||||||
|
!!currentUserAdminPermissionRoutes[config.routePath];
|
||||||
}
|
}
|
||||||
function updateDedupeTotalDataAccess() {
|
function refreshAdminTabAccess() {
|
||||||
var canUse = currentUserRole === 'super_admin' ||
|
Object.keys(ADMIN_TAB_ACCESS_CONFIG).forEach(function(tabName) {
|
||||||
!!currentUserAdminPermissionKeys['admin_dedupe_total_data'] ||
|
applyTabAccess(tabName, hasAdminTabAccess(tabName));
|
||||||
!!currentUserAdminPermissionRoutes['dedupe-total-data'];
|
});
|
||||||
applyTabAccess('dedupe-total-data', canUse);
|
|
||||||
}
|
|
||||||
function updateShopManageAccess() {
|
|
||||||
var canUse = currentUserRole === 'super_admin' ||
|
|
||||||
!!currentUserAdminPermissionKeys['admin_shop_manage'] ||
|
|
||||||
!!currentUserAdminPermissionRoutes['shop-manage'];
|
|
||||||
applyTabAccess('shop-manage', canUse);
|
|
||||||
}
|
}
|
||||||
function loadCurrentUserAdminPermissions() {
|
function loadCurrentUserAdminPermissions() {
|
||||||
currentUserAdminPermissionKeys = {};
|
currentUserAdminPermissionKeys = {};
|
||||||
currentUserAdminPermissionRoutes = {};
|
currentUserAdminPermissionRoutes = {};
|
||||||
updateAdminOnlyAccess();
|
|
||||||
if (!currentUserId || currentUserRole === 'super_admin') {
|
if (!currentUserId || currentUserRole === 'super_admin') {
|
||||||
updateDedupeTotalDataAccess();
|
refreshAdminTabAccess();
|
||||||
updateShopManageAccess();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fetch('/api/admin/user/' + currentUserId + '/column-permissions?menu_type=admin')
|
fetch('/api/admin/user/' + currentUserId + '/column-permissions?menu_type=admin')
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(res) {
|
.then(function(res) {
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
updateAdminOnlyAccess();
|
refreshAdminTabAccess();
|
||||||
updateDedupeTotalDataAccess();
|
|
||||||
updateShopManageAccess();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(res.items || []).forEach(function(item) {
|
(res.items || []).forEach(function(item) {
|
||||||
@@ -835,14 +912,10 @@
|
|||||||
if (columnKey) currentUserAdminPermissionKeys[columnKey] = true;
|
if (columnKey) currentUserAdminPermissionKeys[columnKey] = true;
|
||||||
if (routePath) currentUserAdminPermissionRoutes[routePath] = true;
|
if (routePath) currentUserAdminPermissionRoutes[routePath] = true;
|
||||||
});
|
});
|
||||||
updateAdminOnlyAccess();
|
refreshAdminTabAccess();
|
||||||
updateDedupeTotalDataAccess();
|
|
||||||
updateShopManageAccess();
|
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
updateAdminOnlyAccess();
|
refreshAdminTabAccess();
|
||||||
updateDedupeTotalDataAccess();
|
|
||||||
updateShopManageAccess();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var allColumnsList = [];
|
var allColumnsList = [];
|
||||||
@@ -1604,7 +1677,11 @@
|
|||||||
var createSel = document.getElementById('shopManageGroupSelect');
|
var createSel = document.getElementById('shopManageGroupSelect');
|
||||||
var editSel = document.getElementById('editShopManageGroupSelect');
|
var editSel = document.getElementById('editShopManageGroupSelect');
|
||||||
var filterSel = document.getElementById('shopManageFilterGroupId');
|
var filterSel = document.getElementById('shopManageFilterGroupId');
|
||||||
|
var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect');
|
||||||
|
var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId');
|
||||||
var selectedFilterId = filterSel ? filterSel.value : '';
|
var selectedFilterId = filterSel ? filterSel.value : '';
|
||||||
|
var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : '';
|
||||||
|
var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : '';
|
||||||
var createOpts = ['<option value="">请选择分组</option>'];
|
var createOpts = ['<option value="">请选择分组</option>'];
|
||||||
var filterOpts = ['<option value="">全部分组</option>'];
|
var filterOpts = ['<option value="">全部分组</option>'];
|
||||||
shopManageGroups.forEach(function(g) {
|
shopManageGroups.forEach(function(g) {
|
||||||
@@ -1614,10 +1691,14 @@
|
|||||||
});
|
});
|
||||||
createSel.innerHTML = createOpts.join('');
|
createSel.innerHTML = createOpts.join('');
|
||||||
editSel.innerHTML = createOpts.join('');
|
editSel.innerHTML = createOpts.join('');
|
||||||
|
if (skipCreateSel) skipCreateSel.innerHTML = createOpts.join('');
|
||||||
if (filterSel) filterSel.innerHTML = filterOpts.join('');
|
if (filterSel) filterSel.innerHTML = filterOpts.join('');
|
||||||
|
if (skipFilterSel) skipFilterSel.innerHTML = filterOpts.join('');
|
||||||
if (selectedCreateId != null) createSel.value = String(selectedCreateId);
|
if (selectedCreateId != null) createSel.value = String(selectedCreateId);
|
||||||
if (selectedEditId != null) editSel.value = String(selectedEditId);
|
if (selectedEditId != null) editSel.value = String(selectedEditId);
|
||||||
if (filterSel && selectedFilterId) filterSel.value = selectedFilterId;
|
if (filterSel && selectedFilterId) filterSel.value = selectedFilterId;
|
||||||
|
if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId;
|
||||||
|
if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadShopManageGroups(selectedCreateId, selectedEditId) {
|
function loadShopManageGroups(selectedCreateId, selectedEditId) {
|
||||||
@@ -1682,6 +1763,7 @@
|
|||||||
loadShopManageGroups().then(function() {
|
loadShopManageGroups().then(function() {
|
||||||
renderShopManageGroupRows();
|
renderShopManageGroupRows();
|
||||||
loadShopManage(shopManagePage);
|
loadShopManage(shopManagePage);
|
||||||
|
loadSkipPriceAsin(skipPriceAsinPage);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1799,6 +1881,7 @@
|
|||||||
loadShopManageGroups().then(function() {
|
loadShopManageGroups().then(function() {
|
||||||
renderShopManageGroupRows();
|
renderShopManageGroupRows();
|
||||||
loadShopManage(shopManagePage);
|
loadShopManage(shopManagePage);
|
||||||
|
loadSkipPriceAsin(skipPriceAsinPage);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
@@ -1888,6 +1971,143 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ========== 版本管理 ==========
|
// ========== 版本管理 ==========
|
||||||
|
// ========== 跳过跟价 ASIN ==========
|
||||||
|
var skipPriceAsinPage = 1, skipPriceAsinPageSize = 15;
|
||||||
|
var skipPriceCountryColumns = [
|
||||||
|
{ code: 'DE', field: 'asin_de', label: '德国' },
|
||||||
|
{ code: 'UK', field: 'asin_uk', label: '英国' },
|
||||||
|
{ code: 'FR', field: 'asin_fr', label: '法国' },
|
||||||
|
{ code: 'IT', field: 'asin_it', label: '意大利' },
|
||||||
|
{ code: 'ES', field: 'asin_es', label: '西班牙' }
|
||||||
|
];
|
||||||
|
function buildSkipPriceAsinQuery(page) {
|
||||||
|
var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize;
|
||||||
|
var groupId = (document.getElementById('skipPriceAsinFilterGroupId').value || '').trim();
|
||||||
|
var shopName = (document.getElementById('skipPriceAsinFilterShopName').value || '').trim();
|
||||||
|
var asin = (document.getElementById('skipPriceAsinFilterAsin').value || '').trim();
|
||||||
|
if (groupId) query += '&group_id=' + encodeURIComponent(groupId);
|
||||||
|
if (shopName) query += '&shop_name=' + encodeURIComponent(shopName);
|
||||||
|
if (asin) query += '&asin=' + encodeURIComponent(asin);
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
function renderSkipPriceAsinCell(item, country) {
|
||||||
|
var value = item[country.field] || '';
|
||||||
|
if (!value) return '<span style="color:#999;">-</span>';
|
||||||
|
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
|
||||||
|
'<span>' + value + '</span>' +
|
||||||
|
'<button class="btn btn-sm btn-danger" data-skip-price-asin-delete="' + item.id + '" data-country="' + country.code + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '"') + '">删除</button>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
function bindSkipPriceAsinActions() {
|
||||||
|
document.querySelectorAll('[data-skip-price-asin-delete]').forEach(function(btn) {
|
||||||
|
btn.onclick = function() {
|
||||||
|
var shopName = (btn.dataset.shopName || '').replace(/"/g, '"');
|
||||||
|
var countryCode = btn.dataset.country || '';
|
||||||
|
if (!confirm('确定删除店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN 吗?')) return;
|
||||||
|
fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinDelete + '/country/' + countryCode, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(res) {
|
||||||
|
if (res.success) loadSkipPriceAsin(skipPriceAsinPage);
|
||||||
|
else alert(res.error || '删除失败');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function loadSkipPriceAsin(page) {
|
||||||
|
skipPriceAsinPage = page || 1;
|
||||||
|
fetch('/api/admin/skip-price-asins?' + buildSkipPriceAsinQuery(skipPriceAsinPage))
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(res) {
|
||||||
|
var tbody = document.getElementById('skipPriceAsinListBody');
|
||||||
|
if (!res.success) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var items = res.items || [];
|
||||||
|
if (items.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无数据</td></tr>';
|
||||||
|
} else {
|
||||||
|
tbody.innerHTML = items.map(function(item, index) {
|
||||||
|
var rowNo = (skipPriceAsinPage - 1) * skipPriceAsinPageSize + index + 1;
|
||||||
|
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td>' +
|
||||||
|
skipPriceCountryColumns.map(function(country) {
|
||||||
|
return '<td>' + renderSkipPriceAsinCell(item, country) + '</td>';
|
||||||
|
}).join('') +
|
||||||
|
'</tr>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
renderPagination('skipPriceAsinPagination', res.total, res.page, res.page_size, loadSkipPriceAsin);
|
||||||
|
bindSkipPriceAsinActions();
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
document.getElementById('skipPriceAsinListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById('btnManageSkipPriceAsinGroups').onclick = openShopManageGroupModal;
|
||||||
|
document.getElementById('btnSearchSkipPriceAsin').onclick = function() {
|
||||||
|
loadSkipPriceAsin(1);
|
||||||
|
};
|
||||||
|
document.getElementById('skipPriceAsinFilterShopName').addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
loadSkipPriceAsin(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('skipPriceAsinFilterAsin').addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
loadSkipPriceAsin(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('btnCreateSkipPriceAsin').onclick = function() {
|
||||||
|
var groupId = (document.getElementById('skipPriceAsinGroupSelect').value || '').trim();
|
||||||
|
var shopName = (document.getElementById('skipPriceAsinShopName').value || '').trim();
|
||||||
|
var asin = (document.getElementById('skipPriceAsinValue').value || '').trim();
|
||||||
|
var countries = Array.from(document.getElementById('skipPriceAsinCountries').selectedOptions).map(function(option) {
|
||||||
|
return option.value;
|
||||||
|
});
|
||||||
|
var msgEl = document.getElementById('msgSkipPriceAsin');
|
||||||
|
msgEl.textContent = '';
|
||||||
|
msgEl.className = 'msg';
|
||||||
|
if (!groupId || !shopName || !asin || !countries.length) {
|
||||||
|
msgEl.textContent = '请完整填写分组、店铺名、国家和 ASIN';
|
||||||
|
msgEl.className = 'msg err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch('/api/admin/skip-price-asin', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
group_id: Number(groupId),
|
||||||
|
shop_name: shopName,
|
||||||
|
countries: countries,
|
||||||
|
asin: asin
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(res) {
|
||||||
|
if (!res.success) {
|
||||||
|
msgEl.textContent = res.error || '保存失败';
|
||||||
|
msgEl.className = 'msg err';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById('skipPriceAsinGroupSelect').value = '';
|
||||||
|
document.getElementById('skipPriceAsinShopName').value = '';
|
||||||
|
document.getElementById('skipPriceAsinValue').value = '';
|
||||||
|
Array.from(document.getElementById('skipPriceAsinCountries').options).forEach(function(option) {
|
||||||
|
option.selected = false;
|
||||||
|
});
|
||||||
|
msgEl.textContent = res.msg || '保存成功';
|
||||||
|
msgEl.className = 'msg ok';
|
||||||
|
loadSkipPriceAsin(1);
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
msgEl.textContent = '请求失败';
|
||||||
|
msgEl.className = 'msg err';
|
||||||
|
});
|
||||||
|
};
|
||||||
function loadVersions() {
|
function loadVersions() {
|
||||||
fetch('/api/admin/versions')
|
fetch('/api/admin/versions')
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
@@ -2199,6 +2419,7 @@
|
|||||||
loadShopKeys(1);
|
loadShopKeys(1);
|
||||||
loadShopManageGroups();
|
loadShopManageGroups();
|
||||||
loadShopManage(1);
|
loadShopManage(1);
|
||||||
|
loadSkipPriceAsin(1);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -181,10 +181,27 @@ function loadTaskSnapshotsFromStorage() { try { const raw = window.localStorage.
|
|||||||
function saveTaskSnapshotsToStorage() { setStorageJson(taskSnapshotsStorageKey(), taskSnapshots.value, Object.keys(taskSnapshots.value).length === 0) }
|
function saveTaskSnapshotsToStorage() { setStorageJson(taskSnapshotsStorageKey(), taskSnapshots.value, Object.keys(taskSnapshots.value).length === 0) }
|
||||||
function loadMatchedItemsFromStorage() { try { const raw = window.localStorage.getItem(matchedItemsStorageKey()); matchedItems.value = raw ? JSON.parse(raw) : [] } catch { matchedItems.value = [] } }
|
function loadMatchedItemsFromStorage() { try { const raw = window.localStorage.getItem(matchedItemsStorageKey()); matchedItems.value = raw ? JSON.parse(raw) : [] } catch { matchedItems.value = [] } }
|
||||||
function saveMatchedItemsToStorage() { setStorageJson(matchedItemsStorageKey(), matchedItems.value, matchedItems.value.length === 0) }
|
function saveMatchedItemsToStorage() { setStorageJson(matchedItemsStorageKey(), matchedItems.value, matchedItems.value.length === 0) }
|
||||||
|
function isTaskMissingError(error: unknown) { const message = error instanceof Error ? error.message : String(error || ''); return message.includes('任务不存在') }
|
||||||
function syncPollingIdsWithTaskState() { const nextIds = pollingTaskIds.value.filter((taskId) => (taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status) === 'RUNNING'); if (nextIds.length === pollingTaskIds.value.length) return; pollingTaskIds.value = nextIds; savePollingIds() }
|
function syncPollingIdsWithTaskState() { const nextIds = pollingTaskIds.value.filter((taskId) => (taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status) === 'RUNNING'); if (nextIds.length === pollingTaskIds.value.length) return; pollingTaskIds.value = nextIds; savePollingIds() }
|
||||||
function addPollingTask(taskId: number) { if (!pollingTaskIds.value.includes(taskId)) { pollingTaskIds.value = [...pollingTaskIds.value, taskId]; savePollingIds() } }
|
function addPollingTask(taskId: number) { if (!pollingTaskIds.value.includes(taskId)) { pollingTaskIds.value = [...pollingTaskIds.value, taskId]; savePollingIds() } }
|
||||||
function clearDispatchTimer(taskId: number) { const timer = dispatchTimers.get(taskId); if (timer) { window.clearTimeout(timer); dispatchTimers.delete(taskId) } }
|
function clearDispatchTimer(taskId: number) { const timer = dispatchTimers.get(taskId); if (timer) { window.clearTimeout(timer); dispatchTimers.delete(taskId) } }
|
||||||
function removePollingTask(taskId: number) { clearDispatchTimer(taskId); pollingTaskIds.value = pollingTaskIds.value.filter((id) => id !== taskId); delete taskDetails.value[taskId]; savePollingIds(); saveTaskDetailsToStorage() }
|
function removePollingTask(taskId: number) {
|
||||||
|
clearDispatchTimer(taskId)
|
||||||
|
pollingTaskIds.value = pollingTaskIds.value.filter((id) => id !== taskId)
|
||||||
|
delete taskDetails.value[taskId]
|
||||||
|
if (taskSnapshots.value[taskId]) {
|
||||||
|
const nextSnapshots = { ...taskSnapshots.value }
|
||||||
|
delete nextSnapshots[taskId]
|
||||||
|
taskSnapshots.value = nextSnapshots
|
||||||
|
saveTaskSnapshotsToStorage()
|
||||||
|
}
|
||||||
|
savePollingIds()
|
||||||
|
saveTaskDetailsToStorage()
|
||||||
|
}
|
||||||
|
function removeTaskLocally(taskId: number) {
|
||||||
|
removePollingTask(taskId)
|
||||||
|
historyItems.value = historyItems.value.filter((row) => normalizeTaskId(row.taskId) !== taskId)
|
||||||
|
}
|
||||||
function stopPollingTask(taskId: number, keepStatus = true) { pollingTaskIds.value = pollingTaskIds.value.filter((id) => id !== taskId); if (!keepStatus) delete taskDetails.value[taskId]; savePollingIds(); saveTaskDetailsToStorage() }
|
function stopPollingTask(taskId: number, keepStatus = true) { pollingTaskIds.value = pollingTaskIds.value.filter((id) => id !== taskId); if (!keepStatus) delete taskDetails.value[taskId]; savePollingIds(); saveTaskDetailsToStorage() }
|
||||||
function isTaskTerminalStatus(status?: string) { return status === 'SUCCESS' || status === 'FAILED' || status === 'COMPLETED' }
|
function isTaskTerminalStatus(status?: string) { return status === 'SUCCESS' || status === 'FAILED' || status === 'COMPLETED' }
|
||||||
function isTaskTerminalById(taskId?: number) { if (!taskId) return false; const status = taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status; return isTaskTerminalStatus(status) }
|
function isTaskTerminalById(taskId?: number) { if (!taskId) return false; const status = taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status; return isTaskTerminalStatus(status) }
|
||||||
@@ -230,12 +247,12 @@ function restoreScheduledDispatches() { const taskIds = new Set<number>(pollingT
|
|||||||
function startScheduleHeartbeat() { if (scheduleHeartbeatTimer) return; scheduleHeartbeatTimer = window.setInterval(() => { void pumpScheduledQueue() }, 1000) }
|
function startScheduleHeartbeat() { if (scheduleHeartbeatTimer) return; scheduleHeartbeatTimer = window.setInterval(() => { void pumpScheduledQueue() }, 1000) }
|
||||||
function stopScheduleHeartbeat() { if (!scheduleHeartbeatTimer) return; window.clearInterval(scheduleHeartbeatTimer); scheduleHeartbeatTimer = null }
|
function stopScheduleHeartbeat() { if (!scheduleHeartbeatTimer) return; window.clearInterval(scheduleHeartbeatTimer); scheduleHeartbeatTimer = null }
|
||||||
function handleScheduleRecovery() { restoreScheduledDispatches(); if (pollingTaskIds.value.length) ensurePolling(true) }
|
function handleScheduleRecovery() { restoreScheduledDispatches(); if (pollingTaskIds.value.length) ensurePolling(true) }
|
||||||
async function refreshTaskBatch() { if (!pollingTaskIds.value.length) return { settledTaskIds: [] as number[] }; try { const batch = await getShopMatchTasksBatch(pollingTaskIds.value); const nextSnapshots = { ...taskSnapshots.value }; const settledTaskIds = new Set<number>(); for (const missingId of batch.missingTaskIds || []) { removePollingTask(missingId); delete nextSnapshots[missingId] } for (const detail of batch.items || []) { const taskId = detail.task?.id; if (!taskId) continue; nextSnapshots[taskId] = detail; const status = detail.task?.status || ''; if (!status) continue; taskDetails.value[taskId] = status; if (isTaskTerminalStatus(status)) { removePollingTask(taskId); settledTaskIds.add(taskId); continue } if (status === 'SCHEDULED') { stopPollingTask(taskId); settledTaskIds.add(taskId) } } taskSnapshots.value = nextSnapshots; saveTaskSnapshotsToStorage(); saveTaskDetailsToStorage(); restoreScheduledDispatches(); return { settledTaskIds: Array.from(settledTaskIds) } } catch { return { settledTaskIds: [] as number[] } } }
|
async function refreshTaskBatch() { if (!pollingTaskIds.value.length) return { settledTaskIds: [] as number[] }; try { const batch = await getShopMatchTasksBatch(pollingTaskIds.value); const nextSnapshots = { ...taskSnapshots.value }; const settledTaskIds = new Set<number>(); for (const missingId of batch.missingTaskIds || []) { removeTaskLocally(missingId); delete nextSnapshots[missingId]; settledTaskIds.add(missingId) } for (const detail of batch.items || []) { const taskId = detail.task?.id; if (!taskId) continue; nextSnapshots[taskId] = detail; const status = detail.task?.status || ''; if (!status) continue; taskDetails.value[taskId] = status; if (isTaskTerminalStatus(status)) { removePollingTask(taskId); settledTaskIds.add(taskId); continue } if (status === 'SCHEDULED') { stopPollingTask(taskId); settledTaskIds.add(taskId) } } taskSnapshots.value = nextSnapshots; saveTaskSnapshotsToStorage(); saveTaskDetailsToStorage(); restoreScheduledDispatches(); return { settledTaskIds: Array.from(settledTaskIds) } } catch { return { settledTaskIds: [] as number[] } } }
|
||||||
function getPollIntervalMs() { return document.visibilityState === 'visible' ? 4000 : 10000 }
|
function getPollIntervalMs() { return document.visibilityState === 'visible' ? 4000 : 10000 }
|
||||||
function scheduleNextPoll(immediate = false) { if (pollTimer.value) { if (!immediate) return; window.clearTimeout(pollTimer.value); pollTimer.value = null } const run = async () => { pollTimer.value = null; if (pollingInFlight.value || !pollingTaskIds.value.length) { if (pollingTaskIds.value.length) scheduleNextPoll(); return } pollingInFlight.value = true; try { const { settledTaskIds } = await refreshTaskBatch(); if (settledTaskIds.length) await Promise.allSettled([loadHistory(), loadDashboard()]) } finally { pollingInFlight.value = false } if (pollingTaskIds.value.length) pollTimer.value = window.setTimeout(run, getPollIntervalMs()) }; if (immediate) void run(); else pollTimer.value = window.setTimeout(run, getPollIntervalMs()) }
|
function scheduleNextPoll(immediate = false) { if (pollTimer.value) { if (!immediate) return; window.clearTimeout(pollTimer.value); pollTimer.value = null } const run = async () => { pollTimer.value = null; if (pollingInFlight.value || !pollingTaskIds.value.length) { if (pollingTaskIds.value.length) scheduleNextPoll(); return } pollingInFlight.value = true; try { const { settledTaskIds } = await refreshTaskBatch(); if (settledTaskIds.length) await Promise.allSettled([loadHistory(), loadDashboard()]) } finally { pollingInFlight.value = false } if (pollingTaskIds.value.length) pollTimer.value = window.setTimeout(run, getPollIntervalMs()) }; if (immediate) void run(); else pollTimer.value = window.setTimeout(run, getPollIntervalMs()) }
|
||||||
function ensurePolling(immediate = false) { scheduleNextPoll(immediate) }
|
function ensurePolling(immediate = false) { scheduleNextPoll(immediate) }
|
||||||
function stopPolling() { if (pollTimer.value) { window.clearTimeout(pollTimer.value); pollTimer.value = null } }
|
function stopPolling() { if (pollTimer.value) { window.clearTimeout(pollTimer.value); pollTimer.value = null } }
|
||||||
async function waitForTaskTerminal(taskId: number) { while (true) { const batch = await getShopMatchTasksBatch([taskId]); const detail = (batch.items || []).find((item) => item.task?.id === taskId); const status = detail?.task?.status || ''; if (detail) { taskSnapshots.value = { ...taskSnapshots.value, [taskId]: detail }; saveTaskSnapshotsToStorage() } if (status) { taskDetails.value[taskId] = status; saveTaskDetailsToStorage() } if (isTaskTerminalStatus(status)) { removePollingTask(taskId); await loadHistory(); await loadDashboard(); restoreScheduledDispatches(); return status } await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) }) } }
|
async function waitForTaskTerminal(taskId: number) { while (true) { const batch = await getShopMatchTasksBatch([taskId]); if ((batch.missingTaskIds || []).includes(taskId)) { removeTaskLocally(taskId); await loadHistory(); await loadDashboard(); restoreScheduledDispatches(); return 'FAILED' } const detail = (batch.items || []).find((item) => item.task?.id === taskId); const status = detail?.task?.status || ''; if (detail) { taskSnapshots.value = { ...taskSnapshots.value, [taskId]: detail }; saveTaskSnapshotsToStorage() } if (status) { taskDetails.value[taskId] = status; saveTaskDetailsToStorage() } if (isTaskTerminalStatus(status)) { removePollingTask(taskId); await loadHistory(); await loadDashboard(); restoreScheduledDispatches(); return status } await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) }) } }
|
||||||
function resolvedTaskStatus(item: ShopMatchHistoryItem) { const taskId = normalizeTaskId(item.taskId); if (!taskId) return item.taskStatus || ''; return taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status || item.taskStatus || '' }
|
function resolvedTaskStatus(item: ShopMatchHistoryItem) { const taskId = normalizeTaskId(item.taskId); if (!taskId) return item.taskStatus || ''; return taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status || item.taskStatus || '' }
|
||||||
function taskSnapshotOf(item: ShopMatchHistoryItem) { const taskId = normalizeTaskId(item.taskId); return taskId ? taskSnapshots.value[taskId] : undefined }
|
function taskSnapshotOf(item: ShopMatchHistoryItem) { const taskId = normalizeTaskId(item.taskId); return taskId ? taskSnapshots.value[taskId] : undefined }
|
||||||
function currentTaskStageText(item: ShopMatchHistoryItem) { const snapshot = taskSnapshotOf(item); const task = snapshot?.task; const stages = task?.scheduleStages || []; if (!stages.length) return ''; const total = stages.length; const activeIndex = typeof task?.activeStageIndex === 'number' ? task.activeStageIndex : undefined; const currentIndex = typeof task?.currentStageIndex === 'number' ? task.currentStageIndex : undefined; if (typeof activeIndex === 'number') return `执行进度: 第 ${activeIndex + 1}/${total} 次执行中`; if (typeof currentIndex === 'number') return `执行进度: 等待第 ${currentIndex + 1}/${total} 次`; return `执行进度: 共 ${total} 次`}
|
function currentTaskStageText(item: ShopMatchHistoryItem) { const snapshot = taskSnapshotOf(item); const task = snapshot?.task; const stages = task?.scheduleStages || []; if (!stages.length) return ''; const total = stages.length; const activeIndex = typeof task?.activeStageIndex === 'number' ? task.activeStageIndex : undefined; const currentIndex = typeof task?.currentStageIndex === 'number' ? task.currentStageIndex : undefined; if (typeof activeIndex === 'number') return `执行进度: 第 ${activeIndex + 1}/${total} 次执行中`; if (typeof currentIndex === 'number') return `执行进度: 等待第 ${currentIndex + 1}/${total} 次`; return `执行进度: 共 ${total} 次`}
|
||||||
@@ -250,10 +267,7 @@ async function deleteTaskRecord(item: ShopMatchHistoryItem) {
|
|||||||
try {
|
try {
|
||||||
if (taskId > 0) {
|
if (taskId > 0) {
|
||||||
await deleteShopMatchTask(taskId)
|
await deleteShopMatchTask(taskId)
|
||||||
removePollingTask(taskId)
|
removeTaskLocally(taskId)
|
||||||
historyItems.value = historyItems.value.filter((row) => normalizeTaskId(row.taskId) !== taskId)
|
|
||||||
delete taskSnapshots.value[taskId]
|
|
||||||
saveTaskSnapshotsToStorage()
|
|
||||||
} else if (resultId > 0) {
|
} else if (resultId > 0) {
|
||||||
await deleteShopMatchHistory(resultId)
|
await deleteShopMatchHistory(resultId)
|
||||||
historyItems.value = historyItems.value.filter((row) => Number(row.resultId || 0) !== resultId)
|
historyItems.value = historyItems.value.filter((row) => Number(row.resultId || 0) !== resultId)
|
||||||
@@ -262,7 +276,14 @@ async function deleteTaskRecord(item: ShopMatchHistoryItem) {
|
|||||||
throw new Error('缺少可删除的任务标识')
|
throw new Error('缺少可删除的任务标识')
|
||||||
}
|
}
|
||||||
await Promise.allSettled([loadHistory(), loadCandidates(), loadDashboard()])
|
await Promise.allSettled([loadHistory(), loadCandidates(), loadDashboard()])
|
||||||
|
ElMessage.success('删除成功')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (taskId > 0 && isTaskMissingError(error)) {
|
||||||
|
removeTaskLocally(taskId)
|
||||||
|
await Promise.allSettled([loadHistory(), loadCandidates(), loadDashboard()])
|
||||||
|
ElMessage.success('任务已在服务端删除,本地记录已同步清理')
|
||||||
|
return
|
||||||
|
}
|
||||||
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
ElMessage.error(error instanceof Error ? error.message : '删除失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user