完成店铺增删改查

This commit is contained in:
super
2026-04-07 14:50:06 +08:00
parent 1b02160b20
commit 2f67b376ee
36 changed files with 1589 additions and 26 deletions

Binary file not shown.

View File

@@ -0,0 +1,60 @@
package com.nanri.aiimage.common.security;
import com.nanri.aiimage.common.exception.BusinessException;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
@Service
public class ShopCredentialCryptoService {
@Value("${aiimage.security.shop-credential-key:change-me-shop-credential-key}")
private String rawKey;
private SecretKeySpec keySpec;
@PostConstruct
public void init() {
try {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] full = sha256.digest(rawKey.getBytes(StandardCharsets.UTF_8));
byte[] key16 = new byte[16];
System.arraycopy(full, 0, key16, 0, 16);
this.keySpec = new SecretKeySpec(key16, "AES");
} catch (Exception ex) {
throw new BusinessException("初始化店铺凭据加密器失败");
}
}
public String encrypt(String plainText) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal((plainText == null ? "" : plainText).getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception ex) {
throw new BusinessException("店铺凭据加密失败");
}
}
public String decrypt(String cipherText) {
try {
if (cipherText == null || cipherText.isBlank()) {
return "";
}
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception ex) {
// 兼容历史数据:旧记录可能是明文或使用旧密钥加密,回退原文避免中断自动化流程。
return cipherText == null ? "" : cipherText;
}
}
}

View File

@@ -0,0 +1,86 @@
package com.nanri.aiimage.modules.productrisk.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.modules.productrisk.model.dto.ProductRiskShopPayloadDto;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Map;
@Service
@RequiredArgsConstructor
public class ProductRiskTaskCacheService {
private static final long PAYLOAD_TTL_HOURS = 24;
private final StringRedisTemplate stringRedisTemplate;
private final ObjectMapper objectMapper;
public ProductRiskShopPayloadDto getShopMergedPayload(Long taskId, String shopKey) {
if (taskId == null || taskId <= 0 || shopKey == null || shopKey.isBlank()) {
return null;
}
Object raw = stringRedisTemplate.opsForHash().get(buildShopPayloadKey(taskId), shopKey);
if (!(raw instanceof String json) || json.isBlank()) {
return null;
}
try {
return objectMapper.readValue(json, ProductRiskShopPayloadDto.class);
} catch (Exception ex) {
throw new BusinessException("读取商品风险店铺缓存失败");
}
}
public void saveShopMergedPayload(Long taskId, String shopKey, ProductRiskShopPayloadDto payload) {
if (taskId == null || taskId <= 0 || shopKey == null || shopKey.isBlank() || payload == null) {
return;
}
try {
stringRedisTemplate.opsForHash().put(buildShopPayloadKey(taskId), shopKey, objectMapper.writeValueAsString(payload));
stringRedisTemplate.expire(buildShopPayloadKey(taskId), Duration.ofHours(PAYLOAD_TTL_HOURS));
} catch (Exception ex) {
throw new BusinessException("暂存商品风险店铺缓存失败");
}
}
public void removeShopMergedPayload(Long taskId, String shopKey) {
if (taskId == null || taskId <= 0 || shopKey == null || shopKey.isBlank()) {
return;
}
stringRedisTemplate.opsForHash().delete(buildShopPayloadKey(taskId), shopKey);
}
public Map<String, ProductRiskShopPayloadDto> getAllShopMergedPayload(Long taskId) {
try {
Map<Object, Object> raw = stringRedisTemplate.opsForHash().entries(buildShopPayloadKey(taskId));
if (raw == null || raw.isEmpty()) {
return Map.of();
}
java.util.LinkedHashMap<String, ProductRiskShopPayloadDto> out = new java.util.LinkedHashMap<>();
for (Map.Entry<Object, Object> entry : raw.entrySet()) {
if (!(entry.getKey() instanceof String key) || !(entry.getValue() instanceof String val) || val.isBlank()) {
continue;
}
out.put(key, objectMapper.readValue(val, ProductRiskShopPayloadDto.class));
}
return out;
} catch (Exception ex) {
throw new BusinessException("读取商品风险缓存失败");
}
}
public void deleteTaskCache(Long taskId) {
if (taskId == null || taskId <= 0) {
return;
}
stringRedisTemplate.delete(buildShopPayloadKey(taskId));
}
private String buildShopPayloadKey(Long taskId) {
return "product-risk:task:shop-payload:" + taskId;
}
}

View File

@@ -56,6 +56,7 @@ public class ProductRiskTaskService {
private final ProductRiskExcelAssemblyService excelAssemblyService;
private final OssStorageService ossStorageService;
private final ObjectMapper objectMapper;
private final ProductRiskTaskCacheService productRiskTaskCacheService;
public ProductRiskDashboardVo dashboard(Long userId) {
if (userId == null || userId <= 0) {
@@ -251,6 +252,10 @@ public class ProductRiskTaskService {
log.warn("[product-risk] compact result json failed: {}", ex.getMessage());
}
fileTaskMapper.updateById(task);
if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus())) {
productRiskTaskCacheService.deleteTaskCache(taskId);
}
}
public ProductRiskTaskBatchVo getTaskDetailsBatch(List<Long> taskIds) {
@@ -402,6 +407,14 @@ public class ProductRiskTaskService {
for (FileResultEntity fr : resultRows) {
String shopKey = fr.getSourceFilename();
ProductRiskShopPayloadDto payload = shopKey == null ? null : payloadByShop.get(shopKey);
// 兼容单店任务:若规范化店名偶发不一致(空格/符号差异)导致未命中,且任务与回传都只有 1 个店时按唯一店回填。
if (payload == null && resultRows.size() == 1 && payloadByShop.size() == 1) {
payload = payloadByShop.values().iterator().next();
log.warn("[product-risk] single-shop fallback matched taskId={} dbShop={} payloadShop={}",
taskId, shopKey, payload.getShopName());
}
if (payload == null) {
// 与删除品牌一致:支持队列逐条处理、分次回传,未在本次 payload 中的店铺保持待处理
continue;
@@ -409,12 +422,20 @@ public class ProductRiskTaskService {
if (payload.getError() != null && !payload.getError().isBlank()) {
markResultFailed(fr, payload.getError());
batchErrors.add(shopKey + ": " + payload.getError());
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
continue;
}
// 分次上报累积:合并到 Redis 缓存,再按“合并后是否完成”决定是否出包。
ProductRiskShopPayloadDto mergedPayload = mergeShopPayload(taskId, shopKey, payload);
if (!isShopPayloadCompleted(mergedPayload)) {
continue;
}
try {
Map<String, List<ProductRiskRowDto>> countries = excelAssemblyService.normalizeCountriesMap(payload.getCountries());
String displayName = payload.getShopName() != null && !payload.getShopName().isBlank()
? payload.getShopName().trim()
Map<String, List<ProductRiskRowDto>> countries = excelAssemblyService.normalizeCountriesMap(mergedPayload.getCountries());
String displayName = mergedPayload.getShopName() != null && !mergedPayload.getShopName().isBlank()
? mergedPayload.getShopName().trim()
: shopKey;
String stem = safeFileStem(displayName);
File xlsx = FileUtil.file(workRoot, stem + ".xlsx");
@@ -432,10 +453,12 @@ public class ProductRiskTaskService {
fileResultMapper.updateById(fr);
FileUtil.del(xlsx);
FileUtil.del(zip);
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
} catch (Exception ex) {
log.warn("[product-risk] shop assemble failed taskId={} shop={} msg={}", taskId, shopKey, ex.getMessage());
markResultFailed(fr, ex.getMessage() == null ? "组装失败" : ex.getMessage());
batchErrors.add(shopKey + ": " + ex.getMessage());
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
}
}
@@ -631,4 +654,155 @@ public class ProductRiskTaskService {
}
return s.replaceAll("[\\\\/:*?\"<>|]", "_");
}
private ProductRiskShopPayloadDto mergeShopPayload(Long taskId, String shopKey, ProductRiskShopPayloadDto incoming) {
ProductRiskShopPayloadDto merged = productRiskTaskCacheService.getShopMergedPayload(taskId, shopKey);
if (merged == null) {
merged = new ProductRiskShopPayloadDto();
}
if (incoming.getShopName() != null && !incoming.getShopName().isBlank()) {
merged.setShopName(incoming.getShopName().trim());
} else if (merged.getShopName() == null || merged.getShopName().isBlank()) {
merged.setShopName(shopKey);
}
if (incoming.getError() != null && !incoming.getError().isBlank()) {
merged.setError(incoming.getError());
}
Map<com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode, List<ProductRiskRowDto>> nextCountries =
merged.getCountries() == null ? new LinkedHashMap<>() : new LinkedHashMap<>(merged.getCountries());
if (incoming.getCountries() != null && !incoming.getCountries().isEmpty()) {
for (Map.Entry<com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode, List<ProductRiskRowDto>> entry : incoming.getCountries().entrySet()) {
com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode code = entry.getKey();
if (code == null) {
continue;
}
List<ProductRiskRowDto> incomingRows = entry.getValue();
if (incomingRows == null || incomingRows.isEmpty()) {
continue;
}
nextCountries.put(code, mergeCountryRows(nextCountries.get(code), incomingRows));
}
}
merged.setCountries(nextCountries);
productRiskTaskCacheService.saveShopMergedPayload(taskId, shopKey, merged);
return merged;
}
private List<ProductRiskRowDto> mergeCountryRows(List<ProductRiskRowDto> existingRows, List<ProductRiskRowDto> incomingRows) {
List<ProductRiskRowDto> safeExisting = existingRows == null ? List.of() : existingRows;
LinkedHashMap<String, ProductRiskRowDto> byKey = new LinkedHashMap<>();
for (ProductRiskRowDto row : safeExisting) {
if (row == null) {
continue;
}
byKey.put(buildRowKey(row), cloneRow(row));
}
for (ProductRiskRowDto row : incomingRows) {
if (row == null) {
continue;
}
String key = buildRowKey(row);
ProductRiskRowDto prev = byKey.get(key);
byKey.put(key, mergeRow(prev, row));
}
return new ArrayList<>(byKey.values());
}
private String buildRowKey(ProductRiskRowDto row) {
String asinSku = row == null || row.getProductAsinSku() == null ? "" : row.getProductAsinSku().trim().toUpperCase(Locale.ROOT);
String removeAsin = row == null || row.getRemoveAsin() == null ? "" : row.getRemoveAsin().trim().toUpperCase(Locale.ROOT);
String status = row == null || row.getStatus() == null ? "" : row.getStatus().trim().toUpperCase(Locale.ROOT);
if (!asinSku.isBlank()) {
return "asinSku:" + asinSku;
}
if (!removeAsin.isBlank()) {
return "removeAsin:" + removeAsin;
}
if (!status.isBlank()) {
return "status:" + status;
}
return "row:" + java.util.UUID.randomUUID();
}
private ProductRiskRowDto mergeRow(ProductRiskRowDto prev, ProductRiskRowDto incoming) {
if (prev == null) {
return cloneRow(incoming);
}
ProductRiskRowDto merged = cloneRow(prev);
merged.setShopName(firstNonBlank(incoming.getShopName(), merged.getShopName()));
merged.setProductAsinSku(firstNonBlank(incoming.getProductAsinSku(), merged.getProductAsinSku()));
merged.setStatus(firstNonBlank(incoming.getStatus(), merged.getStatus()));
merged.setDone(firstNonBlank(incoming.getDone(), merged.getDone()));
merged.setRemoveAsin(firstNonBlank(incoming.getRemoveAsin(), merged.getRemoveAsin()));
merged.setRemoveStatus(firstNonBlank(incoming.getRemoveStatus(), merged.getRemoveStatus()));
return merged;
}
private ProductRiskRowDto cloneRow(ProductRiskRowDto row) {
ProductRiskRowDto out = new ProductRiskRowDto();
if (row == null) {
return out;
}
out.setShopName(row.getShopName());
out.setProductAsinSku(row.getProductAsinSku());
out.setStatus(row.getStatus());
out.setDone(row.getDone());
out.setRemoveAsin(row.getRemoveAsin());
out.setRemoveStatus(row.getRemoveStatus());
return out;
}
private String firstNonBlank(String preferred, String fallback) {
if (preferred != null && !preferred.isBlank()) {
return preferred;
}
return fallback;
}
private boolean isShopPayloadCompleted(ProductRiskShopPayloadDto payload) {
if (payload == null || payload.getCountries() == null || payload.getCountries().isEmpty()) {
return false;
}
boolean hasAnyRow = false;
for (List<ProductRiskRowDto> rows : payload.getCountries().values()) {
if (rows == null || rows.isEmpty()) {
continue;
}
for (ProductRiskRowDto row : rows) {
if (row == null) {
continue;
}
hasAnyRow = true;
if (!isDoneValue(row.getDone())) {
return false;
}
}
}
return hasAnyRow;
}
private boolean isDoneValue(String value) {
if (value == null) {
return false;
}
String normalized = value.trim().toLowerCase(Locale.ROOT);
if (normalized.isBlank()) {
return false;
}
return "true".equals(normalized)
|| "1".equals(normalized)
|| "yes".equals(normalized)
|| "y".equals(normalized)
|| "".equals(normalized)
|| "完成".equals(normalized)
|| "已完成".equals(normalized);
}
}

View File

@@ -0,0 +1,115 @@
package com.nanri.aiimage.modules.shopkey.controller;
import com.nanri.aiimage.common.api.ApiResponse;
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.dto.ShopManageGroupCreateRequest;
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupUpdateRequest;
import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageItemVo;
import com.nanri.aiimage.modules.shopkey.model.vo.ShopManagePageVo;
import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageGroupItemVo;
import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageCredentialVo;
import com.nanri.aiimage.modules.shopkey.service.ShopManageService;
import com.nanri.aiimage.modules.shopkey.service.ShopManageGroupService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
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.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.PutMapping;
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;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin/shop-manages")
@Tag(name = "店铺管理", description = "维护店铺信息,支持增删改查。")
public class ShopManageController {
@Value("${aiimage.security.internal-token:}")
private String internalToken;
private final ShopManageService shopManageService;
private final ShopManageGroupService shopManageGroupService;
@GetMapping
@Operation(summary = "分页查询店铺", description = "分页查询店铺列表。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = ShopManagePageVo.class)))
})
public ApiResponse<ShopManagePageVo> page(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "15") Long pageSize) {
return ApiResponse.success(shopManageService.page(page, pageSize));
}
@PostMapping
@Operation(summary = "新增店铺", description = "新增一条店铺记录。")
public ApiResponse<ShopManageItemVo> create(@Valid @RequestBody ShopManageCreateRequest request) {
return ApiResponse.success("创建成功", shopManageService.create(request));
}
@PutMapping("/{id}")
@Operation(summary = "更新店铺", description = "按 ID 更新一条店铺记录。")
public ApiResponse<ShopManageItemVo> update(
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
@Valid @RequestBody ShopManageUpdateRequest request) {
return ApiResponse.success("更新成功", shopManageService.update(id, request));
}
@DeleteMapping("/{id}")
@Operation(summary = "删除店铺", description = "按 ID 删除一条店铺记录。")
public ApiResponse<Void> delete(@Parameter(description = "主键ID", required = true) @PathVariable Long id) {
shopManageService.delete(id);
return ApiResponse.success("删除成功", null);
}
@GetMapping("/groups")
@Operation(summary = "查询分组列表", description = "用于店铺管理下拉选择。")
public ApiResponse<java.util.List<ShopManageGroupItemVo>> listGroups() {
return ApiResponse.success(shopManageGroupService.list());
}
@PostMapping("/groups")
@Operation(summary = "新增分组", description = "新增一条店铺分组。")
public ApiResponse<ShopManageGroupItemVo> createGroup(@Valid @RequestBody ShopManageGroupCreateRequest request) {
return ApiResponse.success("创建成功", shopManageGroupService.create(request));
}
@PutMapping("/groups/{id}")
@Operation(summary = "更新分组", description = "按 ID 更新分组名称。")
public ApiResponse<ShopManageGroupItemVo> updateGroup(
@Parameter(description = "主键ID", required = true) @PathVariable Long id,
@Valid @RequestBody ShopManageGroupUpdateRequest request) {
return ApiResponse.success("更新成功", shopManageGroupService.update(id, request));
}
@DeleteMapping("/groups/{id}")
@Operation(summary = "删除分组", description = "删除分组(分组下有店铺时不允许删除)。")
public ApiResponse<Void> deleteGroup(@Parameter(description = "主键ID", required = true) @PathVariable Long id) {
shopManageGroupService.delete(id);
return ApiResponse.success("删除成功", null);
}
@GetMapping("/credential")
@Operation(summary = "按店铺名获取明文凭据(内部)", description = "仅供 Python 自动化内部调用,需传 X-Internal-Token。")
public ApiResponse<ShopManageCredentialVo> getCredentialByShopName(
@RequestParam("shopName") String shopName,
@RequestHeader(value = "X-Internal-Token", required = false) String token) {
if (internalToken == null || internalToken.isBlank() || token == null || !internalToken.equals(token)) {
throw new com.nanri.aiimage.common.exception.BusinessException("无权访问凭据");
}
return ApiResponse.success(shopManageService.getCredentialByShopName(shopName));
}
}

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.ShopManageGroupEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ShopManageGroupMapper extends BaseMapper<ShopManageGroupEntity> {
}

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.ShopManageEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ShopManageMapper extends BaseMapper<ShopManageEntity> {
}

View File

@@ -0,0 +1,21 @@
package com.nanri.aiimage.modules.shopkey.model.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class ShopManageCreateRequest {
@NotNull(message = "分组不能为空")
private Long groupId;
@NotBlank(message = "店铺名不能为空")
private String shopName;
@NotBlank(message = "账号不能为空")
private String account;
@NotBlank(message = "密码不能为空")
private String password;
}

View File

@@ -0,0 +1,11 @@
package com.nanri.aiimage.modules.shopkey.model.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class ShopManageGroupCreateRequest {
@NotBlank(message = "分组名称不能为空")
private String groupName;
}

View File

@@ -0,0 +1,11 @@
package com.nanri.aiimage.modules.shopkey.model.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class ShopManageGroupUpdateRequest {
@NotBlank(message = "分组名称不能为空")
private String groupName;
}

View File

@@ -0,0 +1,21 @@
package com.nanri.aiimage.modules.shopkey.model.dto;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class ShopManageUpdateRequest {
@NotNull(message = "分组不能为空")
private Long groupId;
@NotBlank(message = "店铺名不能为空")
private String shopName;
@NotBlank(message = "账号不能为空")
private String account;
@NotBlank(message = "密码不能为空")
private String password;
}

View File

@@ -0,0 +1,23 @@
package com.nanri.aiimage.modules.shopkey.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("biz_shop_manage")
public class ShopManageEntity {
@TableId(type = IdType.AUTO)
private Long id;
private Long groupId;
private String groupName;
private String shopName;
private String account;
private String password;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,19 @@
package com.nanri.aiimage.modules.shopkey.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("biz_shop_manage_group")
public class ShopManageGroupEntity {
@TableId(type = IdType.AUTO)
private Long id;
private String groupName;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,13 @@
package com.nanri.aiimage.modules.shopkey.model.vo;
import lombok.Data;
@Data
public class ShopManageCredentialVo {
private Long id;
private Long groupId;
private String groupName;
private String shopName;
private String account;
private String password;
}

View File

@@ -0,0 +1,13 @@
package com.nanri.aiimage.modules.shopkey.model.vo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ShopManageGroupItemVo {
private Long id;
private String groupName;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,19 @@
package com.nanri.aiimage.modules.shopkey.model.vo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ShopManageItemVo {
private Long id;
private Long groupId;
private String groupName;
private String shopName;
private String account;
private String password;
private String passwordMasked;
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 ShopManagePageVo {
private List<ShopManageItemVo> items = new ArrayList<>();
private Long total;
private Long page;
private Long pageSize;
}

View File

@@ -0,0 +1,98 @@
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.ShopManageGroupMapper;
import com.nanri.aiimage.modules.shopkey.mapper.ShopManageMapper;
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupCreateRequest;
import com.nanri.aiimage.modules.shopkey.model.dto.ShopManageGroupUpdateRequest;
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageEntity;
import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageGroupItemVo;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ShopManageGroupService {
private final ShopManageGroupMapper groupMapper;
private final ShopManageMapper shopManageMapper;
public List<ShopManageGroupItemVo> list() {
return groupMapper.selectList(new LambdaQueryWrapper<ShopManageGroupEntity>().orderByAsc(ShopManageGroupEntity::getId))
.stream()
.map(this::toVo)
.toList();
}
@Transactional
public ShopManageGroupItemVo create(ShopManageGroupCreateRequest request) {
String groupName = normalizeRequired(request.getGroupName(), "分组名称不能为空");
ensureGroupNameUnique(groupName, null);
ShopManageGroupEntity entity = new ShopManageGroupEntity();
entity.setGroupName(groupName);
groupMapper.insert(entity);
return toVo(getById(entity.getId()));
}
@Transactional
public ShopManageGroupItemVo update(Long id, ShopManageGroupUpdateRequest request) {
ShopManageGroupEntity entity = getById(id);
String groupName = normalizeRequired(request.getGroupName(), "分组名称不能为空");
ensureGroupNameUnique(groupName, id);
entity.setGroupName(groupName);
groupMapper.updateById(entity);
return toVo(getById(id));
}
@Transactional
public void delete(Long id) {
ShopManageGroupEntity entity = getById(id);
Long useCount = shopManageMapper.selectCount(new LambdaQueryWrapper<ShopManageEntity>().eq(ShopManageEntity::getGroupId, entity.getId()));
if (useCount != null && useCount > 0) {
throw new BusinessException("该分组下存在店铺,无法删除");
}
groupMapper.deleteById(entity.getId());
}
public ShopManageGroupEntity getById(Long id) {
ShopManageGroupEntity entity = groupMapper.selectById(id);
if (entity == null) {
throw new BusinessException("分组不存在");
}
return entity;
}
private void ensureGroupNameUnique(String groupName, Long excludeId) {
LambdaQueryWrapper<ShopManageGroupEntity> query = new LambdaQueryWrapper<ShopManageGroupEntity>()
.eq(ShopManageGroupEntity::getGroupName, groupName);
if (excludeId != null) {
query.ne(ShopManageGroupEntity::getId, excludeId);
}
Long count = groupMapper.selectCount(query);
if (count != null && count > 0) {
throw new BusinessException("分组名称已存在");
}
}
private String normalizeRequired(String value, String message) {
String normalized = value == null ? "" : value.trim();
if (normalized.isEmpty()) {
throw new BusinessException(message);
}
return normalized;
}
private ShopManageGroupItemVo toVo(ShopManageGroupEntity entity) {
ShopManageGroupItemVo vo = new ShopManageGroupItemVo();
vo.setId(entity.getId());
vo.setGroupName(entity.getGroupName());
vo.setCreatedAt(entity.getCreatedAt());
vo.setUpdatedAt(entity.getUpdatedAt());
return vo;
}
}

View File

@@ -0,0 +1,178 @@
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.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.ShopManageEntity;
import com.nanri.aiimage.modules.shopkey.model.entity.ShopManageGroupEntity;
import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageItemVo;
import com.nanri.aiimage.modules.shopkey.model.vo.ShopManagePageVo;
import com.nanri.aiimage.modules.shopkey.model.vo.ShopManageCredentialVo;
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;
@Service
@RequiredArgsConstructor
public class ShopManageService {
private final ShopManageMapper shopManageMapper;
private final ShopManageGroupService shopManageGroupService;
private final ShopCredentialCryptoService shopCredentialCryptoService;
public ShopManagePageVo page(long page, long pageSize) {
long safePage = Math.max(page, 1);
long safePageSize = Math.min(Math.max(pageSize, 1), 100);
LambdaQueryWrapper<ShopManageEntity> query = new LambdaQueryWrapper<ShopManageEntity>()
.orderByDesc(ShopManageEntity::getId);
Long total = shopManageMapper.selectCount(query);
List<ShopManageEntity> rows = shopManageMapper
.selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize));
Map<Long, String> groupNameById = buildGroupNameMap(rows.stream()
.map(ShopManageEntity::getGroupId)
.filter(id -> id != null && id > 0)
.distinct()
.toList());
List<ShopManageItemVo> items = rows.stream()
.map(entity -> toItemVo(entity, groupNameById.get(entity.getGroupId())))
.toList();
ShopManagePageVo vo = new ShopManagePageVo();
vo.setItems(items);
vo.setTotal(total);
vo.setPage(safePage);
vo.setPageSize(safePageSize);
return vo;
}
@Transactional
public ShopManageItemVo create(ShopManageCreateRequest request) {
ShopManageGroupEntity group = shopManageGroupService.getById(request.getGroupId());
String shopName = normalizeRequired(request.getShopName(), "店铺名不能为空");
ensureShopNameUnique(shopName, null);
ShopManageEntity entity = new ShopManageEntity();
entity.setGroupId(group.getId());
entity.setGroupName(group.getGroupName());
entity.setShopName(shopName);
entity.setAccount(normalizeRequired(request.getAccount(), "账号不能为空"));
entity.setPassword(shopCredentialCryptoService.encrypt(normalizeRequired(request.getPassword(), "密码不能为空")));
shopManageMapper.insert(entity);
ShopManageEntity saved = getById(entity.getId());
return toItemVo(saved, group.getGroupName());
}
@Transactional
public ShopManageItemVo update(Long id, ShopManageUpdateRequest request) {
ShopManageEntity entity = getById(id);
ShopManageGroupEntity group = shopManageGroupService.getById(request.getGroupId());
String shopName = normalizeRequired(request.getShopName(), "店铺名不能为空");
ensureShopNameUnique(shopName, id);
entity.setGroupId(group.getId());
entity.setGroupName(group.getGroupName());
entity.setShopName(shopName);
entity.setAccount(normalizeRequired(request.getAccount(), "账号不能为空"));
entity.setPassword(shopCredentialCryptoService.encrypt(normalizeRequired(request.getPassword(), "密码不能为空")));
shopManageMapper.updateById(entity);
ShopManageEntity updated = getById(id);
return toItemVo(updated, group.getGroupName());
}
@Transactional
public void delete(Long id) {
ShopManageEntity entity = getById(id);
shopManageMapper.deleteById(entity.getId());
}
public ShopManageCredentialVo getCredentialByShopName(String shopName) {
String normalizedShopName = normalizeRequired(shopName, "店铺名不能为空");
ShopManageEntity entity = shopManageMapper.selectOne(new LambdaQueryWrapper<ShopManageEntity>()
.eq(ShopManageEntity::getShopName, normalizedShopName)
.last("limit 1"));
if (entity == null) {
throw new BusinessException("店铺不存在");
}
ShopManageCredentialVo vo = new ShopManageCredentialVo();
vo.setId(entity.getId());
vo.setGroupId(entity.getGroupId());
vo.setShopName(entity.getShopName());
vo.setAccount(entity.getAccount());
vo.setPassword(shopCredentialCryptoService.decrypt(entity.getPassword()));
try {
ShopManageGroupEntity group = shopManageGroupService.getById(entity.getGroupId());
vo.setGroupName(group.getGroupName());
} catch (Exception ignored) {
vo.setGroupName("");
}
return vo;
}
private ShopManageEntity getById(Long id) {
ShopManageEntity entity = shopManageMapper.selectById(id);
if (entity == null) {
throw new BusinessException("店铺不存在");
}
return entity;
}
private String normalizeRequired(String value, String message) {
String normalized = value == null ? "" : value.trim();
if (normalized.isEmpty()) {
throw new BusinessException(message);
}
return normalized;
}
private void ensureShopNameUnique(String shopName, Long excludeId) {
LambdaQueryWrapper<ShopManageEntity> query = new LambdaQueryWrapper<ShopManageEntity>()
.eq(ShopManageEntity::getShopName, shopName);
if (excludeId != null) {
query.ne(ShopManageEntity::getId, excludeId);
}
Long count = shopManageMapper.selectCount(query);
if (count != null && count > 0) {
throw new BusinessException("店铺名已存在");
}
}
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 g = shopManageGroupService.getById(groupId);
map.put(groupId, g.getGroupName());
} catch (Exception ignored) {
map.put(groupId, "");
}
}
return map;
}
private ShopManageItemVo toItemVo(ShopManageEntity entity, String groupName) {
ShopManageItemVo vo = new ShopManageItemVo();
vo.setId(entity.getId());
vo.setGroupId(entity.getGroupId());
vo.setGroupName(groupName == null ? "" : groupName);
vo.setShopName(entity.getShopName());
vo.setAccount(entity.getAccount());
String masked = entity.getPassword() == null || entity.getPassword().isBlank() ? "" : "******";
// 兼容旧前端字段password 继续返回脱敏值
vo.setPassword(masked);
vo.setPasswordMasked(masked);
vo.setCreatedAt(entity.getCreatedAt());
vo.setUpdatedAt(entity.getUpdatedAt());
return vo;
}
}

View File

@@ -86,6 +86,9 @@ aiimage:
enabled: ${AIIMAGE_MODULE_CLEANUP_ENABLED:true}
cron: ${AIIMAGE_MODULE_CLEANUP_CRON:0 0 0 * * *}
module-types: ${AIIMAGE_MODULE_CLEANUP_MODULE_TYPES:DEDUPE,SPLIT,CONVERT,DELETE_BRAND,PRODUCT_RISK_RESOLVE}
security:
shop-credential-key: ${AIIMAGE_SHOP_CREDENTIAL_KEY:change-me-shop-credential-key}
internal-token: ${AIIMAGE_INTERNAL_TOKEN:}
ziniao:
enabled: ${AIIMAGE_ZINIAO_ENABLED:false}
base-url: ${AIIMAGE_ZINIAO_BASE_URL:https://sbappstoreapi.ziniao.com/openapi-router}

View File

@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS biz_shop_manage (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
group_name VARCHAR(128) NOT NULL COMMENT '分组',
shop_name VARCHAR(128) NOT NULL COMMENT '店铺名',
account VARCHAR(128) NOT NULL COMMENT '账号',
password VARCHAR(255) NOT NULL COMMENT '密码',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
KEY idx_group_name (group_name),
KEY idx_shop_name (shop_name),
KEY idx_created_at (created_at)
) COMMENT='店铺管理表';

View File

@@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS biz_shop_manage_group (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
group_name VARCHAR(128) NOT NULL COMMENT '分组名称',
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_name (group_name),
KEY idx_created_at (created_at)
) COMMENT='店铺管理分组表';
ALTER TABLE biz_shop_manage
ADD COLUMN IF NOT EXISTS group_id BIGINT NULL COMMENT '分组ID' AFTER id;
ALTER TABLE biz_shop_manage
ADD KEY IF NOT EXISTS idx_group_id (group_id);

View File

@@ -0,0 +1,35 @@
-- 兼容旧库:确保 biz_shop_manage 存在 group_id 字段与索引
SET @db_name = DATABASE();
-- 1) 新增 group_id 字段(仅当不存在时)
SET @col_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'biz_shop_manage'
AND COLUMN_NAME = 'group_id'
);
SET @sql_add_col := IF(@col_exists = 0,
'ALTER TABLE biz_shop_manage ADD COLUMN group_id BIGINT NULL COMMENT ''分组ID'' AFTER id',
'SELECT 1'
);
PREPARE stmt FROM @sql_add_col;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 2) 新增 group_id 索引(仅当不存在时)
SET @idx_exists := (
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'biz_shop_manage'
AND INDEX_NAME = 'idx_group_id'
);
SET @sql_add_idx := IF(@idx_exists = 0,
'ALTER TABLE biz_shop_manage ADD KEY idx_group_id (group_id)',
'SELECT 1'
);
PREPARE stmt2 FROM @sql_add_idx;
EXECUTE stmt2;
DEALLOCATE PREPARE stmt2;

View File

@@ -0,0 +1,5 @@
-- 兼容旧库biz_shop_manage 早期存在 group_name 非空字段,新代码改为 group_id 后插入会报错。
-- 这里确保 group_name 可为空,避免旧字段约束阻断写入。
ALTER TABLE biz_shop_manage
MODIFY COLUMN group_name VARCHAR(128) NULL COMMENT '旧字段(兼容保留)';

View File

@@ -0,0 +1,16 @@
-- 将旧表 biz_shop_manage 的 group_name 数据回填到新分组表与 group_id 字段
INSERT INTO biz_shop_manage_group (group_name)
SELECT DISTINCT sm.group_name
FROM biz_shop_manage sm
LEFT JOIN biz_shop_manage_group g ON g.group_name = sm.group_name
WHERE sm.group_name IS NOT NULL
AND TRIM(sm.group_name) <> ''
AND g.id IS NULL;
UPDATE biz_shop_manage sm
JOIN biz_shop_manage_group g ON g.group_name = sm.group_name
SET sm.group_id = g.id
WHERE sm.group_id IS NULL
AND sm.group_name IS NOT NULL
AND TRIM(sm.group_name) <> '';