完成店铺增删改查
This commit is contained in:
3
app/.env
3
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
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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='店铺管理表';
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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 '旧字段(兼容保留)';
|
||||
@@ -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) <> '';
|
||||
Binary file not shown.
@@ -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):
|
||||
|
||||
Binary file not shown.
@@ -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/<int:item_id>', 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/<int:item_id>', 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/<int:item_id>', 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/<int:item_id>', 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 '删除成功'})
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
<div class="tab" data-tab="columns">栏目权限配置</div>
|
||||
<div class="tab" data-tab="dedupe-total-data">数据去重总数据</div>
|
||||
<div class="tab" data-tab="shop-keys">店铺密钥管理</div>
|
||||
<div class="tab" data-tab="shop-manage">店铺管理</div>
|
||||
<div class="tab" data-tab="history">查看生成记录</div>
|
||||
<div class="tab" data-tab="version">版本管理</div>
|
||||
</div>
|
||||
@@ -345,6 +346,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 店铺管理 -->
|
||||
<div id="panel-shop-manage" class="tab-panel">
|
||||
<div class="form-box">
|
||||
<h3 style="margin-bottom:16px;font-size:15px;">新增店铺</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="min-width:220px;">
|
||||
<label>分组</label>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<select id="shopManageGroupSelect" style="min-width:150px;">
|
||||
<option value="">请选择分组</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary" id="btnManageShopGroups" type="button">管理分组</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="min-width:160px;">
|
||||
<label>店铺名</label>
|
||||
<input type="text" id="shopManageShopName" placeholder="请输入店铺名称">
|
||||
</div>
|
||||
<div class="form-group" style="min-width:180px;">
|
||||
<label>账号</label>
|
||||
<input type="text" id="shopManageAccount" placeholder="请输入账号">
|
||||
</div>
|
||||
<div class="form-group" style="min-width:180px;">
|
||||
<label>密码</label>
|
||||
<input type="text" id="shopManagePassword" placeholder="请输入密码">
|
||||
</div>
|
||||
<button class="btn" id="btnCreateShopManage">新增店铺</button>
|
||||
</div>
|
||||
<p class="msg" id="msgShopManage"></p>
|
||||
</div>
|
||||
<div class="panel-box">
|
||||
<h3 style="margin-bottom:16px;font-size:15px;">店铺列表</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>序号</th>
|
||||
<th>分组</th>
|
||||
<th>店铺名</th>
|
||||
<th>账号</th>
|
||||
<th>密码</th>
|
||||
<th>创建时间</th>
|
||||
<th>修改时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="shopManageListBody"></tbody>
|
||||
</table>
|
||||
<div class="pagination" id="shopManagePagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看生成记录 -->
|
||||
<div id="panel-history" class="tab-panel">
|
||||
<div class="form-box">
|
||||
@@ -460,6 +512,66 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分组管理弹窗 -->
|
||||
<div class="modal-mask" id="shopManageGroupModal">
|
||||
<div class="modal" style="max-width:760px;">
|
||||
<h3>分组管理</h3>
|
||||
<div class="form-row">
|
||||
<input type="hidden" id="shopManageGroupEditId">
|
||||
<div class="form-group" style="min-width:260px;">
|
||||
<label>分组名称</label>
|
||||
<input type="text" id="shopManageGroupInput" placeholder="请输入分组名称">
|
||||
</div>
|
||||
<button class="btn" id="btnSaveShopManageGroup" type="button">保存分组</button>
|
||||
<button class="btn btn-secondary" id="btnCancelShopManageGroupEdit" type="button">取消编辑</button>
|
||||
</div>
|
||||
<p class="msg" id="msgShopManageGroup"></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>序号</th><th>分组名称</th><th>创建时间</th><th>修改时间</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody id="shopManageGroupListBody"></tbody>
|
||||
</table>
|
||||
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end;">
|
||||
<button class="btn btn-secondary" id="btnCloseShopManageGroupModal" type="button">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑店铺管理弹窗 -->
|
||||
<div class="modal-mask" id="editShopManageModal">
|
||||
<div class="modal">
|
||||
<h3>编辑店铺</h3>
|
||||
<input type="hidden" id="editShopManageId">
|
||||
<div class="form-group">
|
||||
<label>分组</label>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<select id="editShopManageGroupSelect" style="min-width:200px;">
|
||||
<option value="">请选择分组</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary" id="btnManageShopGroupsFromEdit" type="button">管理分组</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>店铺名</label>
|
||||
<input type="text" id="editShopManageShopName" placeholder="店铺名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>账号</label>
|
||||
<input type="text" id="editShopManageAccount" placeholder="账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="text" id="editShopManagePassword" placeholder="密码">
|
||||
</div>
|
||||
<p class="msg" id="msgEditShopManage"></p>
|
||||
<div style="margin-top:16px;display:flex;gap:8px;">
|
||||
<button class="btn" id="btnSaveShopManage">保存</button>
|
||||
<button class="btn btn-secondary" id="btnCloseEditShopManage">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑店铺密钥弹窗 -->
|
||||
<div class="modal-mask" id="editShopKeyModal">
|
||||
<div class="modal">
|
||||
@@ -495,6 +607,7 @@
|
||||
else if (t.dataset.tab === 'columns') loadColumns();
|
||||
else if (t.dataset.tab === 'dedupe-total-data') loadDedupeTotalData(1);
|
||||
else if (t.dataset.tab === 'shop-keys') loadShopKeys(1);
|
||||
else if (t.dataset.tab === 'shop-manage') loadShopManage(1);
|
||||
else if (t.dataset.tab === 'history') loadHistory(1);
|
||||
else if (t.dataset.tab === 'version') loadVersions();
|
||||
};
|
||||
@@ -1329,6 +1442,281 @@
|
||||
document.getElementById('editShopKeyModal').classList.remove('show');
|
||||
};
|
||||
|
||||
// ========== 店铺管理 ==========
|
||||
var shopManagePage = 1, shopManagePageSize = 15;
|
||||
var shopManageGroups = [];
|
||||
|
||||
function buildShopManageQuery(page) {
|
||||
return 'page=' + (page || 1) + '&page_size=' + shopManagePageSize;
|
||||
}
|
||||
|
||||
function refreshShopGroupSelects(selectedCreateId, selectedEditId) {
|
||||
var createSel = document.getElementById('shopManageGroupSelect');
|
||||
var editSel = document.getElementById('editShopManageGroupSelect');
|
||||
var opts = ['<option value="">请选择分组</option>'];
|
||||
shopManageGroups.forEach(function(g) {
|
||||
opts.push('<option value="' + g.id + '">' + (g.group_name || '') + '</option>');
|
||||
});
|
||||
createSel.innerHTML = opts.join('');
|
||||
editSel.innerHTML = opts.join('');
|
||||
if (selectedCreateId != null) createSel.value = String(selectedCreateId);
|
||||
if (selectedEditId != null) editSel.value = String(selectedEditId);
|
||||
}
|
||||
|
||||
function loadShopManageGroups(selectedCreateId, selectedEditId) {
|
||||
return fetch('/api/admin/shop-manage-groups')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (!res.success) throw new Error(res.error || '加载分组失败');
|
||||
shopManageGroups = res.items || [];
|
||||
refreshShopGroupSelects(selectedCreateId, selectedEditId);
|
||||
return shopManageGroups;
|
||||
})
|
||||
.catch(function() {
|
||||
shopManageGroups = [];
|
||||
refreshShopGroupSelects();
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function openShopManageGroupModal() {
|
||||
document.getElementById('shopManageGroupEditId').value = '';
|
||||
document.getElementById('shopManageGroupInput').value = '';
|
||||
document.getElementById('msgShopManageGroup').textContent = '';
|
||||
document.getElementById('msgShopManageGroup').className = 'msg';
|
||||
loadShopManageGroups().then(function() {
|
||||
renderShopManageGroupRows();
|
||||
document.getElementById('shopManageGroupModal').classList.add('show');
|
||||
});
|
||||
}
|
||||
|
||||
function renderShopManageGroupRows() {
|
||||
var tbody = document.getElementById('shopManageGroupListBody');
|
||||
if (!shopManageGroups.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无分组</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = shopManageGroups.map(function(item, index) {
|
||||
return '<tr><td>' + (index + 1) + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
|
||||
'<button class="btn btn-sm" data-shop-group-edit="' + item.id + '" data-shop-group-name="' + (item.group_name || '').replace(/"/g, '"') + '">编辑</button> ' +
|
||||
'<button class="btn btn-sm btn-danger" data-shop-group-delete="' + item.id + '" data-shop-group-name="' + (item.group_name || '').replace(/"/g, '"') + '">删除</button>' +
|
||||
'</td></tr>';
|
||||
}).join('');
|
||||
|
||||
document.querySelectorAll('[data-shop-group-edit]').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
document.getElementById('shopManageGroupEditId').value = btn.dataset.shopGroupEdit;
|
||||
document.getElementById('shopManageGroupInput').value = (btn.dataset.shopGroupName || '').replace(/"/g, '"');
|
||||
};
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-shop-group-delete]').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
var gid = btn.dataset.shopGroupDelete;
|
||||
var gname = (btn.dataset.shopGroupName || '').replace(/"/g, '"');
|
||||
if (!confirm('确定删除分组「' + gname + '」吗?')) return;
|
||||
fetch('/api/admin/shop-manage-group/' + gid, { method: 'DELETE' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (!res.success) {
|
||||
alert(res.error || '删除失败');
|
||||
return;
|
||||
}
|
||||
loadShopManageGroups().then(function() {
|
||||
renderShopManageGroupRows();
|
||||
loadShopManage(shopManagePage);
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function loadShopManage(page) {
|
||||
shopManagePage = page || 1;
|
||||
fetch('/api/admin/shop-manages?' + buildShopManageQuery(shopManagePage))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
var tbody = document.getElementById('shopManageListBody');
|
||||
if (!res.success) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
var items = res.items || [];
|
||||
if (items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">暂无店铺</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = items.map(function(item, index) {
|
||||
var rowNo = (shopManagePage - 1) * shopManagePageSize + index + 1;
|
||||
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td><td>' + (item.account || '') + '</td><td>' + (item.password || '') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
|
||||
'<button class="btn btn-sm" data-shop-manage-edit="' + item.id + '" data-shop-manage="' + (JSON.stringify(item).replace(/"/g, '"')) + '">编辑</button> ' +
|
||||
'<button class="btn btn-sm btn-danger" data-shop-manage-delete="' + item.id + '" data-shop-manage-name="' + (item.shop_name || '').replace(/"/g, '"') + '">删除</button>' +
|
||||
'</td></tr>';
|
||||
}).join('');
|
||||
}
|
||||
renderPagination('shopManagePagination', res.total, res.page, res.page_size, loadShopManage);
|
||||
bindShopManageActions();
|
||||
})
|
||||
.catch(function() {
|
||||
document.getElementById('shopManageListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
|
||||
});
|
||||
}
|
||||
|
||||
function bindShopManageActions() {
|
||||
document.querySelectorAll('[data-shop-manage-edit]').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
var item = {};
|
||||
try { item = JSON.parse((btn.dataset.shopManage || '').replace(/"/g, '"')); } catch (e) { item = {}; }
|
||||
document.getElementById('editShopManageId').value = item.id || '';
|
||||
document.getElementById('editShopManageShopName').value = item.shop_name || '';
|
||||
document.getElementById('editShopManageAccount').value = item.account || '';
|
||||
document.getElementById('editShopManagePassword').value = item.password || '';
|
||||
document.getElementById('msgEditShopManage').textContent = '';
|
||||
document.getElementById('msgEditShopManage').className = 'msg';
|
||||
loadShopManageGroups(null, item.group_id).then(function() {
|
||||
document.getElementById('editShopManageModal').classList.add('show');
|
||||
});
|
||||
};
|
||||
});
|
||||
document.querySelectorAll('[data-shop-manage-delete]').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
var name = (btn.dataset.shopManageName || '').replace(/"/g, '"');
|
||||
if (!confirm('确定删除店铺「' + name + '」吗?')) return;
|
||||
fetch('/api/admin/shop-manage/' + btn.dataset.shopManageDelete, { method: 'DELETE' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) { loadShopManage(shopManagePage); }
|
||||
else { alert(res.error || '删除失败'); }
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btnManageShopGroups').onclick = openShopManageGroupModal;
|
||||
document.getElementById('btnManageShopGroupsFromEdit').onclick = openShopManageGroupModal;
|
||||
document.getElementById('btnCloseShopManageGroupModal').onclick = function() {
|
||||
document.getElementById('shopManageGroupModal').classList.remove('show');
|
||||
};
|
||||
document.getElementById('btnCancelShopManageGroupEdit').onclick = function() {
|
||||
document.getElementById('shopManageGroupEditId').value = '';
|
||||
document.getElementById('shopManageGroupInput').value = '';
|
||||
};
|
||||
document.getElementById('btnSaveShopManageGroup').onclick = function() {
|
||||
var editId = (document.getElementById('shopManageGroupEditId').value || '').trim();
|
||||
var groupName = (document.getElementById('shopManageGroupInput').value || '').trim();
|
||||
var msgEl = document.getElementById('msgShopManageGroup');
|
||||
msgEl.textContent = '';
|
||||
msgEl.className = 'msg';
|
||||
if (!groupName) {
|
||||
msgEl.textContent = '请输入分组名称';
|
||||
msgEl.className = 'msg err';
|
||||
return;
|
||||
}
|
||||
var method = editId ? 'PUT' : 'POST';
|
||||
var url = editId ? ('/api/admin/shop-manage-group/' + editId) : '/api/admin/shop-manage-group';
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ group_name: groupName })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (!res.success) {
|
||||
msgEl.textContent = res.error || '保存失败';
|
||||
msgEl.className = 'msg err';
|
||||
return;
|
||||
}
|
||||
document.getElementById('shopManageGroupEditId').value = '';
|
||||
document.getElementById('shopManageGroupInput').value = '';
|
||||
msgEl.textContent = res.msg || '保存成功';
|
||||
msgEl.className = 'msg ok';
|
||||
loadShopManageGroups().then(function() {
|
||||
renderShopManageGroupRows();
|
||||
loadShopManage(shopManagePage);
|
||||
});
|
||||
})
|
||||
.catch(function() {
|
||||
msgEl.textContent = '请求失败';
|
||||
msgEl.className = 'msg err';
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById('btnCreateShopManage').onclick = function() {
|
||||
var groupId = (document.getElementById('shopManageGroupSelect').value || '').trim();
|
||||
var shopName = (document.getElementById('shopManageShopName').value || '').trim();
|
||||
var account = (document.getElementById('shopManageAccount').value || '').trim();
|
||||
var password = (document.getElementById('shopManagePassword').value || '').trim();
|
||||
var msgEl = document.getElementById('msgShopManage');
|
||||
msgEl.textContent = '';
|
||||
msgEl.className = 'msg';
|
||||
if (!groupId || !shopName || !account || !password) {
|
||||
msgEl.textContent = '请完整填写分组、店铺名、账号、密码';
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
fetch('/api/admin/shop-manage', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, account: account, password: password })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) {
|
||||
document.getElementById('shopManageGroupSelect').value = '';
|
||||
document.getElementById('shopManageShopName').value = '';
|
||||
document.getElementById('shopManageAccount').value = '';
|
||||
document.getElementById('shopManagePassword').value = '';
|
||||
msgEl.textContent = res.msg || '创建成功';
|
||||
msgEl.className = 'msg ok';
|
||||
loadShopManage(1);
|
||||
} else {
|
||||
msgEl.textContent = res.error || '创建失败';
|
||||
msgEl.className = 'msg err';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
msgEl.textContent = '请求失败';
|
||||
msgEl.className = 'msg err';
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById('btnSaveShopManage').onclick = function() {
|
||||
var itemId = document.getElementById('editShopManageId').value;
|
||||
var groupId = (document.getElementById('editShopManageGroupSelect').value || '').trim();
|
||||
var shopName = (document.getElementById('editShopManageShopName').value || '').trim();
|
||||
var account = (document.getElementById('editShopManageAccount').value || '').trim();
|
||||
var password = (document.getElementById('editShopManagePassword').value || '').trim();
|
||||
var msgEl = document.getElementById('msgEditShopManage');
|
||||
msgEl.textContent = '';
|
||||
msgEl.className = 'msg';
|
||||
if (!groupId || !shopName || !account || !password) {
|
||||
msgEl.textContent = '请完整填写分组、店铺名、账号、密码';
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
fetch('/api/admin/shop-manage/' + itemId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, account: account, password: password })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) {
|
||||
document.getElementById('editShopManageModal').classList.remove('show');
|
||||
loadShopManage(shopManagePage);
|
||||
} else {
|
||||
msgEl.textContent = res.error || '保存失败';
|
||||
msgEl.classList.add('err');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
msgEl.textContent = '请求失败';
|
||||
msgEl.classList.add('err');
|
||||
});
|
||||
};
|
||||
document.getElementById('btnCloseEditShopManage').onclick = function() {
|
||||
document.getElementById('editShopManageModal').classList.remove('show');
|
||||
};
|
||||
|
||||
// ========== 版本管理 ==========
|
||||
function loadVersions() {
|
||||
fetch('/api/admin/versions')
|
||||
@@ -1530,6 +1918,8 @@
|
||||
loadColumnsForPermission();
|
||||
loadDedupeTotalData(1);
|
||||
loadShopKeys(1);
|
||||
loadShopManageGroups();
|
||||
loadShopManage(1);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -400,8 +400,6 @@ async function saveTemplateFromUrl(url: string, fallbackFilename: string, bridge
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
async function downloadTemplateXlsx() {
|
||||
@@ -489,8 +487,6 @@ async function downloadConvertResult(item: ConvertResultItem) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
window.open(item.downloadUrl, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -344,8 +344,6 @@ async function downloadCleanResult(item: DedupeResultItem) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
window.open(item.downloadUrl, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
||||
<div v-if="getQueueStatus(item)" class="files">队列状态:{{ getQueueStatus(item) }}</div>
|
||||
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount
|
||||
}}</div>
|
||||
}}</div>
|
||||
<div v-if="item.totalRows !== undefined && item.matched" class="time">去重后 {{ item.totalRows }} 条
|
||||
</div>
|
||||
<div v-if="shouldShowProgress(item)" class="delete-brand-progress-block">
|
||||
@@ -161,7 +161,7 @@
|
||||
<div class="files">匹配结果:{{ formatMatchResult(item) }}</div>
|
||||
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
||||
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount
|
||||
}}</div>
|
||||
}}</div>
|
||||
<div v-if="item.totalRows !== undefined && item.matched" class="time">去重后 {{ item.totalRows }} 条
|
||||
</div>
|
||||
<div v-if="shouldShowProgress(item)" class="delete-brand-progress-block">
|
||||
@@ -1055,8 +1055,6 @@ async function downloadTaskResult(item: DeleteBrandResultItem) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
async function loadHistory() {
|
||||
|
||||
@@ -828,16 +828,18 @@ async function downloadResult(item: ProductRiskHistoryItem) {
|
||||
const api = getPywebviewApi()
|
||||
const url = getProductRiskResultDownloadUrl(item.resultId)
|
||||
const filename = item.outputFilename || `${item.shopName || 'result'}.zip`
|
||||
if (api?.save_file_from_url_new) {
|
||||
const result = await api.save_file_from_url_new(url, filename)
|
||||
if (result.success) {
|
||||
ElMessage.success(`已保存:${result.path || filename}`)
|
||||
} else if (result.error && result.error !== '用户取消') {
|
||||
ElMessage.error(result.error)
|
||||
}
|
||||
|
||||
if (!api?.save_file_from_url_new) {
|
||||
ElMessage.error('当前客户端未提供 save_file_from_url_new,无法下载文件')
|
||||
return
|
||||
}
|
||||
window.open(url, '_blank')
|
||||
|
||||
const result = await api.save_file_from_url_new(url, filename)
|
||||
if (result.success) {
|
||||
ElMessage.success(`已保存:${result.path || filename}`)
|
||||
} else if (result.error && result.error !== '用户取消') {
|
||||
ElMessage.error(result.error)
|
||||
}
|
||||
}
|
||||
|
||||
function formatMatchStatus(status: string | undefined) {
|
||||
|
||||
@@ -363,8 +363,6 @@ async function downloadSplitResult(item: SplitResultItem) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
window.open(item.downloadUrl, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user