完善后台接口字段

This commit is contained in:
super
2026-04-07 17:57:03 +08:00
parent acff31e652
commit 336338cff5
25 changed files with 181 additions and 25 deletions

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@
<link rel="modulepreload" crossorigin href="/assets/pywebview-V_UIajJm.js">
<link rel="modulepreload" crossorigin href="/assets/brand-1nUVsWrm.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-Q4glvYu5.css">
<link rel="stylesheet" crossorigin href="/assets/convert-CQ22EBpm.css">
<link rel="stylesheet" crossorigin href="/assets/convert-7wWJ02Tw.css">
</head>
<body>
<div id="app"></div>

View File

@@ -8,7 +8,7 @@
<link rel="modulepreload" crossorigin href="/assets/pywebview-V_UIajJm.js">
<link rel="modulepreload" crossorigin href="/assets/brand-1nUVsWrm.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-Q4glvYu5.css">
<link rel="stylesheet" crossorigin href="/assets/dedupe-DyzjKbsT.css">
<link rel="stylesheet" crossorigin href="/assets/dedupe-BpNHwt51.css">
</head>
<body>
<div id="app"></div>

View File

@@ -8,7 +8,7 @@
<link rel="modulepreload" crossorigin href="/assets/pywebview-V_UIajJm.js">
<link rel="modulepreload" crossorigin href="/assets/brand-1nUVsWrm.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-Q4glvYu5.css">
<link rel="stylesheet" crossorigin href="/assets/delete-brand-C-Rj3NMH.css">
<link rel="stylesheet" crossorigin href="/assets/delete-brand-Dl7T0pmm.css">
</head>
<body>
<div id="app"></div>

View File

@@ -8,7 +8,7 @@
<link rel="modulepreload" crossorigin href="/assets/pywebview-V_UIajJm.js">
<link rel="modulepreload" crossorigin href="/assets/brand-1nUVsWrm.js">
<link rel="stylesheet" crossorigin href="/assets/pywebview-Q4glvYu5.css">
<link rel="stylesheet" crossorigin href="/assets/split-RNZYJ-zZ.css">
<link rel="stylesheet" crossorigin href="/assets/split-CRUIYKS6.css">
</head>
<body>
<div id="app"></div>

View File

@@ -229,6 +229,9 @@ public class ProductRiskTaskService {
task.setSuccessFileCount(ok);
task.setFailedFileCount(fail);
task.setUpdatedAt(LocalDateTime.now());
log.info("[product-risk] reconcile summary taskId={} ok={} fail={} allDone={}",
taskId, ok, fail, allDone);
if (!allDone) {
task.setStatus("RUNNING");
task.setErrorMessage(null);
@@ -399,10 +402,17 @@ public class ProductRiskTaskService {
payloadByShop.put(key, p);
}
}
log.info("[product-risk] submitResult received taskId={} payloadShopCount={} payloadKeys={}",
taskId, payloadByShop.size(), payloadByShop.keySet());
File workRoot = FileUtil.mkdir(FileUtil.file(System.getProperty("java.io.tmpdir"), "product-risk-result", String.valueOf(taskId)));
List<String> batchErrors = new ArrayList<>();
int matchedShopCount = 0;
int skippedUnmatchedCount = 0;
int waitingCount = 0;
int assembledCount = 0;
int fallbackMatchedCount = 0;
for (FileResultEntity fr : resultRows) {
String shopKey = fr.getSourceFilename();
@@ -411,14 +421,17 @@ public class ProductRiskTaskService {
// 兼容单店任务:若规范化店名偶发不一致(空格/符号差异)导致未命中,且任务与回传都只有 1 个店时按唯一店回填。
if (payload == null && resultRows.size() == 1 && payloadByShop.size() == 1) {
payload = payloadByShop.values().iterator().next();
fallbackMatchedCount++;
log.warn("[product-risk] single-shop fallback matched taskId={} dbShop={} payloadShop={}",
taskId, shopKey, payload.getShopName());
}
if (payload == null) {
skippedUnmatchedCount++;
// 与删除品牌一致:支持队列逐条处理、分次回传,未在本次 payload 中的店铺保持待处理
continue;
}
matchedShopCount++;
if (payload.getError() != null && !payload.getError().isBlank()) {
markResultFailed(fr, payload.getError());
batchErrors.add(shopKey + ": " + payload.getError());
@@ -428,7 +441,16 @@ public class ProductRiskTaskService {
// 分次上报累积:合并到 Redis 缓存,再按“合并后是否完成”决定是否出包。
ProductRiskShopPayloadDto mergedPayload = mergeShopPayload(taskId, shopKey, payload);
if (!isShopPayloadCompleted(mergedPayload)) {
boolean shopDone = isShopPayloadCompleted(mergedPayload);
log.info("[product-risk] shop merged taskId={} shop={} incomingDone={} mergedDone={} mergedRows={} payloadCountries={}",
taskId,
shopKey,
payloadHasAnyDoneTrue(payload),
shopDone,
countPayloadRows(mergedPayload),
mergedPayload.getCountries() == null ? 0 : mergedPayload.getCountries().size());
if (!shopDone) {
waitingCount++;
continue;
}
@@ -451,6 +473,9 @@ public class ProductRiskTaskService {
fr.setSuccess(1);
fr.setErrorMessage(null);
fileResultMapper.updateById(fr);
assembledCount++;
log.info("[product-risk] shop assembled taskId={} shop={} rowCount={} file={}",
taskId, shopKey, fr.getRowCount(), fr.getResultFilename());
FileUtil.del(xlsx);
FileUtil.del(zip);
productRiskTaskCacheService.removeShopMergedPayload(taskId, shopKey);
@@ -708,6 +733,20 @@ public class ProductRiskTaskService {
if (row == null) {
continue;
}
// 兼容 Python 末次“空行 done=true”心跳视为当前国家已有行全部完成。
if (isDoneValue(row.getDone()) && isEmptyBusinessRow(row) && !byKey.isEmpty()) {
for (Map.Entry<String, ProductRiskRowDto> entry : byKey.entrySet()) {
ProductRiskRowDto existed = entry.getValue();
ProductRiskRowDto doneMerged = mergeRow(existed, row);
if (doneMerged.getDone() == null || doneMerged.getDone().isBlank()) {
doneMerged.setDone("true");
}
entry.setValue(doneMerged);
}
continue;
}
String key = buildRowKey(row);
ProductRiskRowDto prev = byKey.get(key);
byKey.put(key, mergeRow(prev, row));
@@ -760,6 +799,22 @@ public class ProductRiskTaskService {
return out;
}
private boolean isEmptyBusinessRow(ProductRiskRowDto row) {
if (row == null) {
return true;
}
// 注意shopName 只是上下文信息,不作为业务行是否为空的判定条件。
// Python 末次心跳行通常只带 shopName + done=true其他业务字段为空。
return isBlank(row.getProductAsinSku())
&& isBlank(row.getRemoveAsin())
&& isBlank(row.getStatus())
&& isBlank(row.getRemoveStatus());
}
private boolean isBlank(String val) {
return val == null || val.trim().isEmpty();
}
private String firstNonBlank(String preferred, String fallback) {
if (preferred != null && !preferred.isBlank()) {
return preferred;
@@ -771,22 +826,61 @@ public class ProductRiskTaskService {
if (payload == null || payload.getCountries() == null || payload.getCountries().isEmpty()) {
return false;
}
boolean hasAnyRow = false;
boolean hasAnyBusinessRow = false;
for (List<ProductRiskRowDto> rows : payload.getCountries().values()) {
if (rows == null || rows.isEmpty()) {
continue;
}
for (ProductRiskRowDto row : rows) {
if (row == null) {
if (row == null || isEmptyBusinessRow(row)) {
continue;
}
hasAnyRow = true;
hasAnyBusinessRow = true;
if (!isDoneValue(row.getDone())) {
return false;
}
}
}
return hasAnyRow;
return hasAnyBusinessRow;
}
private int countPayloadRows(ProductRiskShopPayloadDto payload) {
if (payload == null || payload.getCountries() == null || payload.getCountries().isEmpty()) {
return 0;
}
int n = 0;
for (List<ProductRiskRowDto> rows : payload.getCountries().values()) {
if (rows == null) {
continue;
}
for (ProductRiskRowDto row : rows) {
if (row == null || isEmptyBusinessRow(row)) {
continue;
}
n++;
}
}
return n;
}
private boolean payloadHasAnyDoneTrue(ProductRiskShopPayloadDto payload) {
if (payload == null || payload.getCountries() == null) {
return false;
}
for (List<ProductRiskRowDto> rows : payload.getCountries().values()) {
if (rows == null) {
continue;
}
for (ProductRiskRowDto row : rows) {
if (row == null || isEmptyBusinessRow(row)) {
continue;
}
if (isDoneValue(row.getDone())) {
return true;
}
}
}
return false;
}
private boolean isDoneValue(String value) {

View File

@@ -13,6 +13,9 @@ public class ShopManageCreateRequest {
@NotBlank(message = "店铺名不能为空")
private String shopName;
@NotBlank(message = "店铺商城名不能为空")
private String mallName;
@NotBlank(message = "账号不能为空")
private String account;

View File

@@ -13,6 +13,9 @@ public class ShopManageUpdateRequest {
@NotBlank(message = "店铺名不能为空")
private String shopName;
@NotBlank(message = "店铺商城名不能为空")
private String mallName;
@NotBlank(message = "账号不能为空")
private String account;

View File

@@ -1,6 +1,7 @@
package com.nanri.aiimage.modules.shopkey.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@@ -16,6 +17,8 @@ public class ShopManageEntity {
private Long groupId;
private String groupName;
private String shopName;
@TableField("mall_name")
private String mallName;
private String account;
private String password;
private LocalDateTime createdAt;

View File

@@ -8,6 +8,7 @@ public class ShopManageCredentialVo {
private Long groupId;
private String groupName;
private String shopName;
private String mallName;
private String account;
private String password;
}

View File

@@ -11,6 +11,7 @@ public class ShopManageItemVo {
private Long groupId;
private String groupName;
private String shopName;
private String mallName;
private String account;
private String password;
private String passwordMasked;

View File

@@ -57,12 +57,14 @@ Map<Long, String> groupNameById = buildGroupNameMap(rows.stream()
public ShopManageItemVo create(ShopManageCreateRequest request) {
ShopManageGroupEntity group = shopManageGroupService.getById(request.getGroupId());
String shopName = normalizeRequired(request.getShopName(), "店铺名不能为空");
String mallName = normalizeRequired(request.getMallName(), "店铺商城名不能为空");
ensureShopNameUnique(shopName, null);
ShopManageEntity entity = new ShopManageEntity();
entity.setGroupId(group.getId());
entity.setGroupName(group.getGroupName());
entity.setShopName(shopName);
entity.setMallName(mallName);
entity.setAccount(normalizeRequired(request.getAccount(), "账号不能为空"));
entity.setPassword(shopCredentialCryptoService.encrypt(normalizeRequired(request.getPassword(), "密码不能为空")));
shopManageMapper.insert(entity);
@@ -75,11 +77,13 @@ Map<Long, String> groupNameById = buildGroupNameMap(rows.stream()
ShopManageEntity entity = getById(id);
ShopManageGroupEntity group = shopManageGroupService.getById(request.getGroupId());
String shopName = normalizeRequired(request.getShopName(), "店铺名不能为空");
String mallName = normalizeRequired(request.getMallName(), "店铺商城名不能为空");
ensureShopNameUnique(shopName, id);
entity.setGroupId(group.getId());
entity.setGroupName(group.getGroupName());
entity.setShopName(shopName);
entity.setMallName(mallName);
entity.setAccount(normalizeRequired(request.getAccount(), "账号不能为空"));
entity.setPassword(shopCredentialCryptoService.encrypt(normalizeRequired(request.getPassword(), "密码不能为空")));
shopManageMapper.updateById(entity);
@@ -105,6 +109,7 @@ Map<Long, String> groupNameById = buildGroupNameMap(rows.stream()
vo.setId(entity.getId());
vo.setGroupId(entity.getGroupId());
vo.setShopName(entity.getShopName());
vo.setMallName(entity.getMallName());
vo.setAccount(entity.getAccount());
vo.setPassword(shopCredentialCryptoService.decrypt(entity.getPassword()));
try {
@@ -166,6 +171,7 @@ Map<Long, String> groupNameById = buildGroupNameMap(rows.stream()
vo.setGroupId(entity.getGroupId());
vo.setGroupName(groupName == null ? "" : groupName);
vo.setShopName(entity.getShopName());
vo.setMallName(entity.getMallName());
vo.setAccount(entity.getAccount());
String masked = entity.getPassword() == null || entity.getPassword().isBlank() ? "" : "******";
// 兼容旧前端字段password 继续返回脱敏值

View File

@@ -0,0 +1,17 @@
SET @db_name = DATABASE();
SET @mall_name_col_exists := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
AND TABLE_NAME = 'biz_shop_manage'
AND COLUMN_NAME = 'mall_name'
);
SET @sql_add_mall_name_col := IF(@mall_name_col_exists = 0,
'ALTER TABLE biz_shop_manage ADD COLUMN mall_name VARCHAR(128) NOT NULL DEFAULT '''' COMMENT ''店铺商城名'' AFTER shop_name',
'SELECT 1'
);
PREPARE stmt_add_mall_name_col FROM @sql_add_mall_name_col;
EXECUTE stmt_add_mall_name_col;
DEALLOCATE PREPARE stmt_add_mall_name_col;

View File

@@ -961,6 +961,7 @@ def list_shop_manages():
'group_id': item.get('groupId'),
'group_name': item.get('groupName') or '',
'shop_name': item.get('shopName') or '',
'mall_name': item.get('mallName') or '',
'account': item.get('account') or '',
'password': item.get('passwordMasked') or '',
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
@@ -984,6 +985,7 @@ def create_shop_manage():
payload = {
'groupId': data.get('group_id'),
'shopName': (data.get('shop_name') or '').strip(),
'mallName': (data.get('mall_name') or '').strip(),
'account': (data.get('account') or '').strip(),
'password': (data.get('password') or '').strip(),
}
@@ -1003,6 +1005,7 @@ def create_shop_manage():
'group_id': item.get('groupId'),
'group_name': item.get('groupName') or '',
'shop_name': item.get('shopName') or '',
'mall_name': item.get('mallName') or '',
'account': item.get('account') or '',
'password': item.get('password') or '',
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
@@ -1018,6 +1021,7 @@ def update_shop_manage(item_id):
payload = {
'groupId': data.get('group_id'),
'shopName': (data.get('shop_name') or '').strip(),
'mallName': (data.get('mall_name') or '').strip(),
'account': (data.get('account') or '').strip(),
'password': (data.get('password') or '').strip(),
}
@@ -1036,6 +1040,7 @@ def update_shop_manage(item_id):
'id': item.get('id'),
'group_name': item.get('groupName') or '',
'shop_name': item.get('shopName') or '',
'mall_name': item.get('mallName') or '',
'account': item.get('account') or '',
'password': item.get('password') or '',
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],

View File

@@ -23,8 +23,8 @@ bucket_path = "nanri-image/"
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
import os
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"

View File

@@ -364,6 +364,10 @@
<label>店铺名</label>
<input type="text" id="shopManageShopName" placeholder="请输入店铺名称">
</div>
<div class="form-group" style="min-width:180px;">
<label>店铺商城名</label>
<input type="text" id="shopManageMallName" placeholder="请输入店铺商城名">
</div>
<div class="form-group" style="min-width:180px;">
<label>账号</label>
<input type="text" id="shopManageAccount" placeholder="请输入账号">
@@ -384,6 +388,7 @@
<th>序号</th>
<th>分组</th>
<th>店铺名</th>
<th>店铺商城名</th>
<th>账号</th>
<th>密码</th>
<th>创建时间</th>
@@ -556,6 +561,10 @@
<label>店铺名</label>
<input type="text" id="editShopManageShopName" placeholder="店铺名">
</div>
<div class="form-group">
<label>店铺商城名</label>
<input type="text" id="editShopManageMallName" placeholder="店铺商城名">
</div>
<div class="form-group">
<label>账号</label>
<input type="text" id="editShopManageAccount" placeholder="账号">
@@ -1538,16 +1547,16 @@
.then(function(res) {
var tbody = document.getElementById('shopManageListBody');
if (!res.success) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
tbody.innerHTML = '<tr><td colspan="9" 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>';
tbody.innerHTML = '<tr><td colspan="9" 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>' +
return '<tr><td>' + rowNo + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.shop_name || '') + '</td><td>' + (item.mall_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, '&quot;')) + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-manage-delete="' + item.id + '" data-shop-manage-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td></tr>';
@@ -1557,7 +1566,7 @@
bindShopManageActions();
})
.catch(function() {
document.getElementById('shopManageListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
document.getElementById('shopManageListBody').innerHTML = '<tr><td colspan="9" class="empty-tip">请求失败</td></tr>';
});
}
@@ -1568,6 +1577,7 @@
try { item = JSON.parse((btn.dataset.shopManage || '').replace(/&quot;/g, '"')); } catch (e) { item = {}; }
document.getElementById('editShopManageId').value = item.id || '';
document.getElementById('editShopManageShopName').value = item.shop_name || '';
document.getElementById('editShopManageMallName').value = item.mall_name || '';
document.getElementById('editShopManageAccount').value = item.account || '';
document.getElementById('editShopManagePassword').value = item.password || '';
document.getElementById('msgEditShopManage').textContent = '';
@@ -1643,26 +1653,28 @@
document.getElementById('btnCreateShopManage').onclick = function() {
var groupId = (document.getElementById('shopManageGroupSelect').value || '').trim();
var shopName = (document.getElementById('shopManageShopName').value || '').trim();
var mallName = (document.getElementById('shopManageMallName').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 = '请完整填写分组、店铺名、账号、密码';
if (!groupId || !shopName || !mallName || !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 })
body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, 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('shopManageMallName').value = '';
document.getElementById('shopManageAccount').value = '';
document.getElementById('shopManagePassword').value = '';
msgEl.textContent = res.msg || '创建成功';
@@ -1683,20 +1695,21 @@
var itemId = document.getElementById('editShopManageId').value;
var groupId = (document.getElementById('editShopManageGroupSelect').value || '').trim();
var shopName = (document.getElementById('editShopManageShopName').value || '').trim();
var mallName = (document.getElementById('editShopManageMallName').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 = '请完整填写分组、店铺名、账号、密码';
if (!groupId || !shopName || !mallName || !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 })
body: JSON.stringify({ group_id: Number(groupId), shop_name: shopName, mall_name: mallName, account: account, password: password })
})
.then(function(r) { return r.json(); })
.then(function(res) {

View File

@@ -37,6 +37,7 @@
<div class="country-pref-checks">
<label v-for="row in countryCheckboxRows" :key="row.code" class="country-check-row">
<input type="checkbox" class="country-check-input" :checked="isCountrySelected(row.code)"
:disabled="isCountrySelectionLocked(row.code)"
@change="onCountryNativeChange(row.code, $event)" />
<span class="country-check-text">{{ row.label }}{{ row.code }}</span>
</label>
@@ -278,9 +279,18 @@ function isCountrySelected(code: string) {
return orderedCountryCodes.value.includes(code)
}
function isCountrySelectionLocked(code: string) {
return orderedCountryCodes.value.length === 1 && orderedCountryCodes.value[0] === code
}
function onCountryNativeChange(code: string, e: Event) {
const el = e.target as HTMLInputElement | null
if (!el) return
if (!el.checked && isCountrySelectionLocked(code)) {
el.checked = true
ElMessage.warning('至少保留 1 个国家')
return
}
onCountryToggle(code, el.checked)
}