diff --git a/app/.env b/app/.env index 085b36a..9ffac95 100644 --- a/app/.env +++ b/app/.env @@ -13,6 +13,7 @@ client_name=ShuFuAI # java_api_base=http://127.0.0.1:18080 -java_api_base=http://8.136.19.173:18080 +java_api_base=http://8.136.19.173:18081 +# java_api_base=http://8.136.19.173:18080 diff --git a/backend-java/sdk-java-5.0.6.jar b/backend-java/sdk-java-5.0.6.jar deleted file mode 100644 index 6af6a20..0000000 Binary files a/backend-java/sdk-java-5.0.6.jar and /dev/null differ diff --git a/backend-java/src/main/java/com/nanri/aiimage/common/security/ShopCredentialCryptoService.java b/backend-java/src/main/java/com/nanri/aiimage/common/security/ShopCredentialCryptoService.java new file mode 100644 index 0000000..00fff2e --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/common/security/ShopCredentialCryptoService.java @@ -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; + } + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskCacheService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskCacheService.java new file mode 100644 index 0000000..69f25f1 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskCacheService.java @@ -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 getAllShopMergedPayload(Long taskId) { + try { + Map raw = stringRedisTemplate.opsForHash().entries(buildShopPayloadKey(taskId)); + if (raw == null || raw.isEmpty()) { + return Map.of(); + } + java.util.LinkedHashMap out = new java.util.LinkedHashMap<>(); + for (Map.Entry 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; + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java index fe64cc6..8307cdc 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/productrisk/service/ProductRiskTaskService.java @@ -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 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> countries = excelAssemblyService.normalizeCountriesMap(payload.getCountries()); - String displayName = payload.getShopName() != null && !payload.getShopName().isBlank() - ? payload.getShopName().trim() + Map> 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> nextCountries = + merged.getCountries() == null ? new LinkedHashMap<>() : new LinkedHashMap<>(merged.getCountries()); + + if (incoming.getCountries() != null && !incoming.getCountries().isEmpty()) { + for (Map.Entry> entry : incoming.getCountries().entrySet()) { + com.nanri.aiimage.modules.productrisk.model.enums.ProductRiskCountryCode code = entry.getKey(); + if (code == null) { + continue; + } + List 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 mergeCountryRows(List existingRows, List incomingRows) { + List safeExisting = existingRows == null ? List.of() : existingRows; + LinkedHashMap 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 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); + } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/ShopManageController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/ShopManageController.java new file mode 100644 index 0000000..c645559 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/controller/ShopManageController.java @@ -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 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 create(@Valid @RequestBody ShopManageCreateRequest request) { + return ApiResponse.success("创建成功", shopManageService.create(request)); + } + + @PutMapping("/{id}") + @Operation(summary = "更新店铺", description = "按 ID 更新一条店铺记录。") + public ApiResponse 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 delete(@Parameter(description = "主键ID", required = true) @PathVariable Long id) { + shopManageService.delete(id); + return ApiResponse.success("删除成功", null); + } + + @GetMapping("/groups") + @Operation(summary = "查询分组列表", description = "用于店铺管理下拉选择。") + public ApiResponse> listGroups() { + return ApiResponse.success(shopManageGroupService.list()); + } + + @PostMapping("/groups") + @Operation(summary = "新增分组", description = "新增一条店铺分组。") + public ApiResponse createGroup(@Valid @RequestBody ShopManageGroupCreateRequest request) { + return ApiResponse.success("创建成功", shopManageGroupService.create(request)); + } + + @PutMapping("/groups/{id}") + @Operation(summary = "更新分组", description = "按 ID 更新分组名称。") + public ApiResponse 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 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 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)); + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopManageGroupMapper.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopManageGroupMapper.java new file mode 100644 index 0000000..fb44b6c --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopManageGroupMapper.java @@ -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 { +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopManageMapper.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopManageMapper.java new file mode 100644 index 0000000..27eca8f --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/mapper/ShopManageMapper.java @@ -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 { +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageCreateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageCreateRequest.java new file mode 100644 index 0000000..71ccced --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageCreateRequest.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupCreateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupCreateRequest.java new file mode 100644 index 0000000..705e1e1 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupCreateRequest.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupUpdateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupUpdateRequest.java new file mode 100644 index 0000000..007b045 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageGroupUpdateRequest.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageUpdateRequest.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageUpdateRequest.java new file mode 100644 index 0000000..193b46c --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/dto/ShopManageUpdateRequest.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageEntity.java new file mode 100644 index 0000000..5f4c00a --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageEntity.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageGroupEntity.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageGroupEntity.java new file mode 100644 index 0000000..3655103 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/entity/ShopManageGroupEntity.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageCredentialVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageCredentialVo.java new file mode 100644 index 0000000..d7dd379 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageCredentialVo.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageGroupItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageGroupItemVo.java new file mode 100644 index 0000000..a3da49e --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageGroupItemVo.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageItemVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageItemVo.java new file mode 100644 index 0000000..6967d77 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManageItemVo.java @@ -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; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManagePageVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManagePageVo.java new file mode 100644 index 0000000..8b14749 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/model/vo/ShopManagePageVo.java @@ -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 items = new ArrayList<>(); + private Long total; + private Long page; + private Long pageSize; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageGroupService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageGroupService.java new file mode 100644 index 0000000..151ea57 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageGroupService.java @@ -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 list() { + return groupMapper.selectList(new LambdaQueryWrapper().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().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 query = new LambdaQueryWrapper() + .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; + } +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java new file mode 100644 index 0000000..6aea8b0 --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/shopkey/service/ShopManageService.java @@ -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 query = new LambdaQueryWrapper() + .orderByDesc(ShopManageEntity::getId); + Long total = shopManageMapper.selectCount(query); + List rows = shopManageMapper + .selectList(query.last("LIMIT " + ((safePage - 1) * safePageSize) + ", " + safePageSize)); + +Map groupNameById = buildGroupNameMap(rows.stream() + .map(ShopManageEntity::getGroupId) + .filter(id -> id != null && id > 0) + .distinct() + .toList()); + + List 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() + .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 query = new LambdaQueryWrapper() + .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 buildGroupNameMap(List groupIds) { + if (groupIds == null || groupIds.isEmpty()) { + return Map.of(); + } + LinkedHashMap 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; + } +} diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml index ecc5f73..8f44981 100644 --- a/backend-java/src/main/resources/application.yml +++ b/backend-java/src/main/resources/application.yml @@ -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} diff --git a/backend-java/src/main/resources/db/V11__create_shop_manage.sql b/backend-java/src/main/resources/db/V11__create_shop_manage.sql new file mode 100644 index 0000000..667e6cc --- /dev/null +++ b/backend-java/src/main/resources/db/V11__create_shop_manage.sql @@ -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='店铺管理表'; diff --git a/backend-java/src/main/resources/db/V12__shop_manage_group.sql b/backend-java/src/main/resources/db/V12__shop_manage_group.sql new file mode 100644 index 0000000..d959aa3 --- /dev/null +++ b/backend-java/src/main/resources/db/V12__shop_manage_group.sql @@ -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); diff --git a/backend-java/src/main/resources/db/V13__ensure_shop_manage_group_id.sql b/backend-java/src/main/resources/db/V13__ensure_shop_manage_group_id.sql new file mode 100644 index 0000000..97844dc --- /dev/null +++ b/backend-java/src/main/resources/db/V13__ensure_shop_manage_group_id.sql @@ -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; diff --git a/backend-java/src/main/resources/db/V14__shop_manage_drop_legacy_group_name.sql b/backend-java/src/main/resources/db/V14__shop_manage_drop_legacy_group_name.sql new file mode 100644 index 0000000..0e87c8b --- /dev/null +++ b/backend-java/src/main/resources/db/V14__shop_manage_drop_legacy_group_name.sql @@ -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 '旧字段(兼容保留)'; diff --git a/backend-java/src/main/resources/db/V15__backfill_shop_manage_group_id.sql b/backend-java/src/main/resources/db/V15__backfill_shop_manage_group_id.sql new file mode 100644 index 0000000..fc51e34 --- /dev/null +++ b/backend-java/src/main/resources/db/V15__backfill_shop_manage_group_id.sql @@ -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) <> ''; diff --git a/backend/__pycache__/config.cpython-312.pyc b/backend/__pycache__/config.cpython-312.pyc index 94466e8..7684111 100644 Binary files a/backend/__pycache__/config.cpython-312.pyc and b/backend/__pycache__/config.cpython-312.pyc differ diff --git a/backend/app.py b/backend/app.py index 7a90dcc..201ace9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -14,7 +14,6 @@ from blueprints.auth import auth from blueprints.main import main from blueprints.admin_api import admin_api from blueprints.version import version_bp -from blueprints.get_resource import get_resource BASE_DIR = os.path.dirname(os.path.abspath(__file__)) app = Flask(__name__, template_folder=BASE_DIR, static_folder=BASE_DIR) @@ -28,7 +27,6 @@ app.register_blueprint(auth) app.register_blueprint(main) app.register_blueprint(admin_api) app.register_blueprint(version_bp) -app.register_blueprint(get_resource) def run_app(host='0.0.0.0', port=15124): diff --git a/backend/blueprints/__pycache__/admin_api.cpython-312.pyc b/backend/blueprints/__pycache__/admin_api.cpython-312.pyc index e4b7967..cc9225f 100644 Binary files a/backend/blueprints/__pycache__/admin_api.cpython-312.pyc and b/backend/blueprints/__pycache__/admin_api.cpython-312.pyc differ diff --git a/backend/blueprints/admin_api.py b/backend/blueprints/admin_api.py index 5e7e477..1497c40 100644 --- a/backend/blueprints/admin_api.py +++ b/backend/blueprints/admin_api.py @@ -938,3 +938,205 @@ def delete_dedupe_total_data(item_id): 'success': True, 'msg': result.get('message') or '删除成功', }) + + +# ---------- 店铺管理 ---------- + +@admin_api.route('/shop-manages') +@admin_required +def list_shop_manages(): + page = max(1, int(request.args.get('page', 1))) + page_size = min(100, max(1, int(request.args.get('page_size', 15)))) + result, error_response, status = _proxy_backend_java( + 'GET', + '/api/admin/shop-manages', + params={'page': page, 'pageSize': page_size}, + ) + 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 '', + 'account': item.get('account') or '', + 'password': item.get('passwordMasked') 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('/shop-manage', methods=['POST']) +@admin_required +def create_shop_manage(): + data = request.get_json() or {} + payload = { + 'groupId': data.get('group_id'), + 'shopName': (data.get('shop_name') or '').strip(), + 'account': (data.get('account') or '').strip(), + 'password': (data.get('password') or '').strip(), + } + result, error_response, status = _proxy_backend_java( + 'POST', + '/api/admin/shop-manages', + 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 '', + 'account': item.get('account') or '', + 'password': item.get('password') or '', + 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], + 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], + }, + }) + + +@admin_api.route('/shop-manage/', methods=['PUT']) +@admin_required +def update_shop_manage(item_id): + data = request.get_json() or {} + payload = { + 'groupId': data.get('group_id'), + 'shopName': (data.get('shop_name') or '').strip(), + 'account': (data.get('account') or '').strip(), + 'password': (data.get('password') or '').strip(), + } + result, error_response, status = _proxy_backend_java( + 'PUT', + f'/api/admin/shop-manages/{item_id}', + 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_name': item.get('groupName') or '', + 'shop_name': item.get('shopName') or '', + 'account': item.get('account') or '', + 'password': item.get('password') or '', + 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], + 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], + }, + }) + + +@admin_api.route('/shop-manage/', methods=['DELETE']) +@admin_required +def delete_shop_manage(item_id): + result, error_response, status = _proxy_backend_java( + 'DELETE', + f'/api/admin/shop-manages/{item_id}', + ) + if error_response is not None: + return error_response, status + return jsonify({ + 'success': True, + 'msg': result.get('message') or '删除成功', + }) + + +@admin_api.route('/shop-manage-groups') +@admin_required +def list_shop_manage_groups(): + result, error_response, status = _proxy_backend_java( + 'GET', + '/api/admin/shop-manages/groups', + ) + if error_response is not None: + return error_response, status + items = [ + { + 'id': item.get('id'), + 'group_name': item.get('groupName') or '', + 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], + 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], + } + for item in (result.get('data') or []) + ] + return jsonify({'success': True, 'items': items}) + + +@admin_api.route('/shop-manage-group', methods=['POST']) +@admin_required +def create_shop_manage_group(): + data = request.get_json() or {} + payload = {'groupName': (data.get('group_name') or '').strip()} + result, error_response, status = _proxy_backend_java( + 'POST', + '/api/admin/shop-manages/groups', + 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_name': item.get('groupName') or '', + 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], + 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], + }, + }) + + +@admin_api.route('/shop-manage-group/', methods=['PUT']) +@admin_required +def update_shop_manage_group(item_id): + data = request.get_json() or {} + payload = {'groupName': (data.get('group_name') or '').strip()} + result, error_response, status = _proxy_backend_java( + 'PUT', + f'/api/admin/shop-manages/groups/{item_id}', + 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_name': item.get('groupName') or '', + 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], + 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], + }, + }) + + +@admin_api.route('/shop-manage-group/', methods=['DELETE']) +@admin_required +def delete_shop_manage_group(item_id): + result, error_response, status = _proxy_backend_java( + 'DELETE', + f'/api/admin/shop-manages/groups/{item_id}', + ) + if error_response is not None: + return error_response, status + return jsonify({'success': True, 'msg': result.get('message') or '删除成功'}) diff --git a/backend/web_source/admin.html b/backend/web_source/admin.html index 78d1c34..668b502 100644 --- a/backend/web_source/admin.html +++ b/backend/web_source/admin.html @@ -113,6 +113,7 @@
栏目权限配置
数据去重总数据
店铺密钥管理
+
店铺管理
查看生成记录
版本管理
@@ -345,6 +346,57 @@ + +
+
+

新增店铺

+
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+

+
+
+

店铺列表

+ + + + + + + + + + + + + + +
序号分组店铺名账号密码创建时间修改时间操作
+ +
+
+
@@ -460,6 +512,66 @@
+ + + + + +