提交工作更新

This commit is contained in:
super
2026-04-13 13:02:41 +08:00
parent 62d30ec190
commit cd84def61d
20 changed files with 932 additions and 60 deletions

View File

@@ -6,12 +6,19 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@RequiredArgsConstructor
@Slf4j
public class PermissionMenuSchemaInitializer {
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
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 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 = ''");
ensureDefaultAdminMenus();
executeQuietly("""
INSERT INTO columns (name, column_key, menu_type, route_path)
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) {
try {
jdbcTemplate.execute(sql);
@@ -52,4 +72,7 @@ public class PermissionMenuSchemaInitializer {
log.debug("[permission-menu] schema init skipped: {}", ex.getMessage());
}
}
private record DefaultAdminMenu(String name, String columnKey, String routePath) {
}
}

View File

@@ -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);
}
}

View File

@@ -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> {
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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'
);