完成后端架构重构等

This commit is contained in:
super
2026-04-23 15:25:41 +08:00
parent 6894f9cc57
commit 0391cb223f
86 changed files with 10843 additions and 1757 deletions

View File

@@ -769,10 +769,10 @@
<option value="ES">西班牙</option>
</select>
</div>
<div class="form-group" style="min-width:260px;flex:1;">
<label>ASIN</label>
<div class="form-group" style="min-width:320px;flex:1;">
<label>ASIN / 最低价</label>
<div id="skipPriceAsinInputs" style="display:flex;flex-direction:column;gap:8px;">
<div style="color:#999;font-size:13px;">请选择国家后输入对应 ASIN</div>
<div style="color:#999;font-size:13px;">请选择国家后输入对应 ASIN 和最低价</div>
</div>
</div>
<button class="btn" id="btnCreateSkipPriceAsin">新增 ASIN</button>
@@ -966,7 +966,7 @@
<label>组员</label>
<select id="shopManageGroupMemberSelect" multiple size="6" style="min-height:140px;">
</select>
<div style="color:#888;font-size:12px;margin-top:6px;">可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。</div>
<div id="shopManageGroupMemberHelp" style="color:#888;font-size:12px;margin-top:6px;">可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。</div>
</div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-bottom:12px;">
@@ -1034,6 +1034,35 @@
</div>
</div>
<div class="modal-mask" id="editSkipPriceAsinModal">
<div class="modal">
<h3>编辑跳过跟价 ASIN</h3>
<input type="hidden" id="editSkipPriceAsinId">
<input type="hidden" id="editSkipPriceAsinCountry">
<div class="form-group">
<label>店铺名</label>
<input type="text" id="editSkipPriceAsinShopName" readonly style="background:#f5f5f5;">
</div>
<div class="form-group">
<label>国家</label>
<input type="text" id="editSkipPriceAsinCountryLabel" readonly style="background:#f5f5f5;">
</div>
<div class="form-group">
<label>ASIN</label>
<input type="text" id="editSkipPriceAsinValue" placeholder="请输入 ASIN">
</div>
<div class="form-group">
<label>最低价</label>
<input type="number" id="editSkipPriceMinimumPrice" min="0" step="0.01" placeholder="请输入最低价,可留空">
</div>
<p class="msg" id="msgEditSkipPriceAsin"></p>
<div style="margin-top:16px;display:flex;gap:8px;">
<button class="btn" id="btnSaveSkipPriceAsin">保存</button>
<button class="btn btn-secondary" id="btnCloseEditSkipPriceAsin">取消</button>
</div>
</div>
</div>
<div class="modal-mask" id="editShopManageModal">
<div class="modal">
<h3>编辑店铺</h3>
@@ -1655,22 +1684,6 @@
document.getElementById('historyListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
});
}
function loadUserOptions() {
fetch('/api/admin/users?page=1&page_size=999')
.then(function (r) { return r.json(); })
.then(function (res) {
var sel = document.getElementById('filterUser');
var cur = sel.value;
sel.innerHTML = '<option value="">全部用户</option>';
(res.items || []).forEach(function (u) {
var opt = document.createElement('option');
opt.value = u.id;
opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')';
sel.appendChild(opt);
});
sel.value = cur || '';
});
}
var shopManageAllUsers = [];
function getEligibleShopManageGroupUsers(leaderUserId) {
var canViewAllMembers = currentUserRole === 'super_admin';
@@ -1683,15 +1696,29 @@
}
function refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds) {
var sel = document.getElementById('shopManageGroupMemberSelect');
var helpEl = document.getElementById('shopManageGroupMemberHelp');
if (!sel) return;
var selectedMap = {};
(selectedUserIds || []).forEach(function (id) {
selectedMap[String(id)] = true;
});
var options = getEligibleShopManageGroupUsers(leaderUserId).map(function (u) {
var eligibleUsers = getEligibleShopManageGroupUsers(leaderUserId);
var options = eligibleUsers.map(function (u) {
return '<option value="' + u.id + '"' + (selectedMap[String(u.id)] ? ' selected' : '') + '>' + (u.username || '') + '</option>';
});
sel.innerHTML = options.join('');
if (options.length) {
sel.innerHTML = options.join('');
if (helpEl) {
helpEl.textContent = '可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。';
}
return;
}
sel.innerHTML = '<option value="" disabled>当前组长暂无可添加组员</option>';
if (helpEl) {
helpEl.textContent = currentUserRole === 'normal'
? '普通账号没有下属普通员工时,这里会为空;当前账号只能作为组长使用。'
: '当前组长名下暂无可添加的普通员工账号。';
}
}
function setShopManageGroupLeader(leaderUserId, leaderUsername, selectedUserIds) {
var leaderIdEl = document.getElementById('shopManageGroupLeaderUserId');
@@ -1712,6 +1739,9 @@
return fetch('/api/admin/users?page=1&page_size=999')
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
throw new Error(res.error || '加载用户失败');
}
var items = res.items || [];
shopManageAllUsers = items.map(function (u) {
return {
@@ -1736,6 +1766,17 @@
sel.appendChild(opt);
});
sel.value = cur || '';
})
.catch(function () {
shopManageAllUsers = [];
var sel = document.getElementById('filterUser');
if (sel) {
sel.innerHTML = '<option value="">全部用户</option>';
}
refreshShopManageGroupMemberSelect(
document.getElementById('shopManageGroupLeaderUserId') ? document.getElementById('shopManageGroupLeaderUserId').value : '',
[]
);
});
}
document.getElementById('btnFilterHistory').onclick = function () { loadHistory(1); };
@@ -2184,6 +2225,7 @@
// ========== 店铺管理 ==========
var shopManagePage = 1, shopManagePageSize = 15;
var shopManageGroups = [];
var currentShopManageGroupGrantRoutes = [];
function buildShopManageQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + shopManagePageSize;
@@ -2313,6 +2355,18 @@
refreshShopManageGroupMemberSelect(currentUserId, []);
}
function setShopManageGroupGrantRoutes(routes) {
currentShopManageGroupGrantRoutes = Array.isArray(routes) ? routes.slice() : [];
}
function updateShopManageGroupButtonsAccess() {
var canManageGroups = !!currentUserId;
['btnManageShopGroups', 'btnManageShopGroupsFromEdit', 'btnManageSkipPriceAsinGroups'].forEach(function (id) {
var btn = document.getElementById(id);
if (btn) btn.style.display = canManageGroups ? '' : 'none';
});
}
function openShopManageGroupModal() {
resetShopManageGroupForm();
loadUserOptions()
@@ -2331,10 +2385,13 @@
}
tbody.innerHTML = shopManageGroups.map(function (item, index) {
var memberNames = Array.isArray(item.member_usernames) ? item.member_usernames.join('、') : '';
var canEdit = currentUserRole === 'super_admin' || String(item.leader_user_id || '') === String(currentUserId || '');
var actionHtml = canEdit
? ('<button class="btn btn-sm" data-shop-group-edit="' + item.id + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-group-delete="' + item.id + '" data-shop-group-name="' + (item.group_name || '').replace(/"/g, '&quot;') + '">删除</button>')
: '<span style="color:#999;">-</span>';
return '<tr><td>' + (index + 1) + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.leader_username || '') + '</td><td>' + (item.member_count || 0) + '</td><td>' + (memberNames || '-') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
'<button class="btn btn-sm" data-shop-group-edit="' + item.id + '">编辑</button> ' +
'<button class="btn btn-sm btn-danger" data-shop-group-delete="' + item.id + '" data-shop-group-name="' + (item.group_name || '').replace(/"/g, '&quot;') + '">删除</button>' +
'</td></tr>';
actionHtml + '</td></tr>';
}).join('');
document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) {
@@ -2378,9 +2435,18 @@
});
}
document.getElementById('btnManageShopGroups').onclick = openShopManageGroupModal;
document.getElementById('btnManageShopGroupsFromEdit').onclick = openShopManageGroupModal;
document.getElementById('btnManageSkipPriceAsinGroups').onclick = openShopManageGroupModal;
document.getElementById('btnManageShopGroups').onclick = function () {
setShopManageGroupGrantRoutes(['shop-manage']);
openShopManageGroupModal();
};
document.getElementById('btnManageShopGroupsFromEdit').onclick = function () {
setShopManageGroupGrantRoutes(['shop-manage']);
openShopManageGroupModal();
};
document.getElementById('btnManageSkipPriceAsinGroups').onclick = function () {
setShopManageGroupGrantRoutes(['skip-price-asin']);
openShopManageGroupModal();
};
document.getElementById('btnSearchShopManage').onclick = function () {
loadShopManage(1);
};
@@ -2411,7 +2477,11 @@
fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group_name: groupName, member_user_ids: memberUserIds })
body: JSON.stringify({
group_name: groupName,
member_user_ids: memberUserIds,
grant_menu_routes: currentShopManageGroupGrantRoutes
})
})
.then(function (r) { return r.json(); })
.then(function (res) {
@@ -2520,11 +2590,11 @@
var skipPriceAsinPage = 1, skipPriceAsinPageSize = 15;
var chooseSkipPriceAsinShopPage = 1, chooseSkipPriceAsinShopPageSize = 10;
var skipPriceCountryColumns = [
{ code: 'DE', field: 'asin_de', label: '德国' },
{ code: 'UK', field: 'asin_uk', label: '英国' },
{ code: 'FR', field: 'asin_fr', label: '法国' },
{ code: 'IT', field: 'asin_it', label: '意大利' },
{ code: 'ES', field: 'asin_es', label: '西班牙' }
{ code: 'DE', field: 'asin_de', minimumPriceField: 'minimum_price_de', label: '德国' },
{ code: 'UK', field: 'asin_uk', minimumPriceField: 'minimum_price_uk', label: '英国' },
{ code: 'FR', field: 'asin_fr', minimumPriceField: 'minimum_price_fr', label: '法国' },
{ code: 'IT', field: 'asin_it', minimumPriceField: 'minimum_price_it', label: '意大利' },
{ code: 'ES', field: 'asin_es', minimumPriceField: 'minimum_price_es', label: '西班牙' }
];
function buildChooseSkipPriceAsinShopQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + chooseSkipPriceAsinShopPageSize;
@@ -2607,41 +2677,68 @@
return option.value;
});
}
function formatSkipPriceMinimumPrice(value) {
if (value === null || value === undefined || value === '') return '';
var num = Number(value);
if (!isFinite(num)) return String(value);
return num.toFixed(2);
}
function getSkipPriceCountryLabel(countryCode) {
var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; });
return country ? country.label : countryCode;
}
function renderSkipPriceAsinInputs() {
var container = document.getElementById('skipPriceAsinInputs');
var selectedCountries = getSelectedSkipPriceCountries();
var existingValues = {};
var existingMinimumPrices = {};
container.querySelectorAll('[data-skip-price-country-input]').forEach(function (input) {
existingValues[input.getAttribute('data-skip-price-country-input')] = input.value;
});
container.querySelectorAll('[data-skip-price-country-minimum-price-input]').forEach(function (input) {
existingMinimumPrices[input.getAttribute('data-skip-price-country-minimum-price-input')] = input.value;
});
if (!selectedCountries.length) {
container.innerHTML = '<div style="color:#999;font-size:13px;">请选择国家后输入 ASIN</div>';
container.innerHTML = '<div style="color:#999;font-size:13px;">请选择国家后输入 ASIN 和最低价</div>';
return;
}
container.innerHTML = selectedCountries.map(function (countryCode) {
var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; });
var label = country ? country.label : countryCode;
var label = getSkipPriceCountryLabel(countryCode);
var value = existingValues[countryCode] || '';
var minimumPrice = existingMinimumPrices[countryCode] || '';
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
'<span style="min-width:56px;color:#555;">' + label + '</span>' +
'<input type="text" data-skip-price-country-input="' + countryCode + '" value="' + value.replace(/"/g, '&quot;') + '" placeholder="请输入' + label + ' ASIN" style="flex:1;min-width:180px;">' +
'<input type="number" data-skip-price-country-minimum-price-input="' + countryCode + '" value="' + minimumPrice.replace(/"/g, '&quot;') + '" min="0" step="0.01" placeholder="最低价" style="width:140px;">' +
'</div>';
}).join('');
}
function collectSkipPriceAsinMappings(countries) {
var mappings = {};
var asinMappings = {};
var minimumPriceMappings = {};
for (var i = 0; i < countries.length; i++) {
var countryCode = countries[i];
var input = document.querySelector('[data-skip-price-country-input="' + countryCode + '"]');
var minimumPriceInput = document.querySelector('[data-skip-price-country-minimum-price-input="' + countryCode + '"]');
var asin = input ? (input.value || '').trim().toUpperCase() : '';
if (!asin) {
var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; });
var label = country ? country.label : countryCode;
var label = getSkipPriceCountryLabel(countryCode);
throw new Error(label + ' ASIN 不能为空');
}
mappings[countryCode] = asin;
var minimumPrice = minimumPriceInput ? (minimumPriceInput.value || '').trim() : '';
if (minimumPrice) {
var minimumPriceNumber = Number(minimumPrice);
if (!isFinite(minimumPriceNumber) || minimumPriceNumber < 0) {
throw new Error(getSkipPriceCountryLabel(countryCode) + ' 最低价格式不正确');
}
minimumPriceMappings[countryCode] = minimumPrice;
}
asinMappings[countryCode] = asin;
}
return mappings;
return {
asinMappings: asinMappings,
minimumPriceMappings: minimumPriceMappings
};
}
function buildSkipPriceAsinQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize;
@@ -2654,37 +2751,49 @@
return query;
}
function renderSkipPriceAsinCell(item, country) {
var value = item[country.field] || '';
if (!value) return '<span style="color:#999;">-</span>';
var asinValue = item[country.field] || '';
var minimumPriceValue = formatSkipPriceMinimumPrice(item[country.minimumPriceField]);
var hasValue = !!asinValue || !!minimumPriceValue;
var infoHtml = hasValue
? ('<div style="display:flex;flex-direction:column;gap:4px;min-width:0;">' +
'<span>' + (asinValue || '<span style="color:#999;">-</span>') + '</span>' +
'<span style="color:#666;font-size:12px;">最低价:' + (minimumPriceValue || '-') + '</span>' +
'</div>')
: '<span style="color:#999;">-</span>';
var deleteHtml = hasValue
? ('<button class="btn btn-sm btn-danger" data-skip-price-asin-delete="' + item.id + '" data-country="' + country.code + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>')
: '';
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
'<span>' + value + '</span>' +
'<button class="btn btn-sm" data-skip-price-asin-edit="' + item.id + '" data-country="' + country.code + '" data-asin="' + value.replace(/"/g, '&quot;') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">编辑</button>' +
'<button class="btn btn-sm btn-danger" data-skip-price-asin-delete="' + item.id + '" data-country="' + country.code + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">删除</button>' +
infoHtml +
'<button class="btn btn-sm" data-skip-price-asin-edit="' + item.id + '" data-country="' + country.code + '" data-asin="' + asinValue.replace(/"/g, '&quot;') + '" data-minimum-price="' + minimumPriceValue.replace(/"/g, '&quot;') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">编辑</button>' +
deleteHtml +
'</div>';
}
function openEditSkipPriceAsinModal(itemId, countryCode, shopName, asinValue, minimumPriceValue) {
document.getElementById('editSkipPriceAsinId').value = itemId || '';
document.getElementById('editSkipPriceAsinCountry').value = countryCode || '';
document.getElementById('editSkipPriceAsinShopName').value = shopName || '';
document.getElementById('editSkipPriceAsinCountryLabel').value = getSkipPriceCountryLabel(countryCode || '');
document.getElementById('editSkipPriceAsinValue').value = asinValue || '';
document.getElementById('editSkipPriceMinimumPrice').value = minimumPriceValue || '';
document.getElementById('msgEditSkipPriceAsin').textContent = '';
document.getElementById('msgEditSkipPriceAsin').className = 'msg';
document.getElementById('editSkipPriceAsinModal').classList.add('show');
}
function bindSkipPriceAsinActions() {
document.querySelectorAll('[data-skip-price-asin-edit]').forEach(function (btn) {
btn.onclick = function () {
var shopName = (btn.dataset.shopName || '').replace(/&quot;/g, '"');
var countryCode = btn.dataset.country || '';
var currentAsin = (btn.dataset.asin || '').replace(/&quot;/g, '"');
var nextAsin = prompt('请输入店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN', currentAsin);
if (nextAsin === null) return;
nextAsin = (nextAsin || '').trim();
if (!nextAsin) {
alert('ASIN 不能为空');
return;
}
fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinEdit + '/country/' + countryCode, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ asin: nextAsin })
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) loadSkipPriceAsin(skipPriceAsinPage);
else alert(res.error || '保存失败');
});
var currentMinimumPrice = (btn.dataset.minimumPrice || '').replace(/&quot;/g, '"');
openEditSkipPriceAsinModal(
btn.dataset.skipPriceAsinEdit,
countryCode,
shopName,
currentAsin,
currentMinimumPrice
);
};
});
document.querySelectorAll('[data-skip-price-asin-delete]').forEach(function (btn) {
@@ -2733,7 +2842,6 @@
document.getElementById('skipPriceAsinListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
});
}
document.getElementById('btnManageSkipPriceAsinGroups').onclick = openShopManageGroupModal;
document.getElementById('btnChooseSkipPriceAsinShop').onclick = openChooseSkipPriceAsinShopModal;
document.getElementById('btnSearchChooseSkipPriceAsinShop').onclick = function () {
loadChooseSkipPriceAsinShops(1);
@@ -2750,6 +2858,58 @@
document.getElementById('btnCloseChooseSkipPriceAsinShopModal').onclick = function () {
document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show');
};
document.getElementById('btnCloseEditSkipPriceAsin').onclick = function () {
document.getElementById('editSkipPriceAsinModal').classList.remove('show');
};
document.getElementById('btnSaveSkipPriceAsin').onclick = function () {
var itemId = (document.getElementById('editSkipPriceAsinId').value || '').trim();
var countryCode = (document.getElementById('editSkipPriceAsinCountry').value || '').trim();
var asin = (document.getElementById('editSkipPriceAsinValue').value || '').trim().toUpperCase();
var minimumPrice = (document.getElementById('editSkipPriceMinimumPrice').value || '').trim();
var msgEl = document.getElementById('msgEditSkipPriceAsin');
msgEl.textContent = '';
msgEl.className = 'msg';
if (!itemId || !countryCode) {
msgEl.textContent = '缺少编辑记录';
msgEl.className = 'msg err';
return;
}
if (!asin) {
msgEl.textContent = 'ASIN 不能为空';
msgEl.className = 'msg err';
return;
}
if (minimumPrice) {
var minimumPriceNumber = Number(minimumPrice);
if (!isFinite(minimumPriceNumber) || minimumPriceNumber < 0) {
msgEl.textContent = '最低价格式不正确';
msgEl.className = 'msg err';
return;
}
}
fetch('/api/admin/skip-price-asin/' + itemId + '/country/' + countryCode, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
asin: asin,
minimum_price: minimumPrice || null
})
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res.success) {
msgEl.textContent = res.error || '保存失败';
msgEl.className = 'msg err';
return;
}
document.getElementById('editSkipPriceAsinModal').classList.remove('show');
loadSkipPriceAsin(skipPriceAsinPage);
})
.catch(function () {
msgEl.textContent = '请求失败';
msgEl.className = 'msg err';
});
};
document.getElementById('skipPriceAsinCountries').addEventListener('change', renderSkipPriceAsinInputs);
document.getElementById('btnSearchSkipPriceAsin').onclick = function () {
loadSkipPriceAsin(1);
@@ -2779,8 +2939,11 @@
return;
}
var asinMappings = {};
var minimumPriceMappings = {};
try {
asinMappings = collectSkipPriceAsinMappings(countries);
var mappings = collectSkipPriceAsinMappings(countries);
asinMappings = mappings.asinMappings || {};
minimumPriceMappings = mappings.minimumPriceMappings || {};
} catch (err) {
msgEl.textContent = err.message || '请输入 ASIN';
msgEl.className = 'msg err';
@@ -2799,7 +2962,8 @@
shop_name: shopName,
countries: countries,
asin: fallbackAsin,
asin_mappings: asinMappings
asin_mappings: asinMappings,
minimum_price_mappings: minimumPriceMappings
})
})
.then(function (r) { return r.json(); })
@@ -3245,13 +3409,19 @@
return;
}
var item = res.item || {};
currentUserId = item.id || currentUserId;
currentUserRole = item.role || currentUserRole;
currentUserUsername = item.username || currentUserUsername;
nameEl.textContent = item.username || '管理员';
if (item.role) {
roleEl.textContent = item.role === 'super_admin' ? '超级管理员' : '管理员';
roleEl.textContent = item.role === 'super_admin'
? '超级管理员'
: (item.role === 'admin' ? '管理员' : '普通账号');
roleEl.style.display = 'inline-block';
} else {
roleEl.style.display = 'none';
}
updateShopManageGroupButtonsAccess();
})
.catch(function () {
fetch('/api/auth/check')
@@ -3259,10 +3429,12 @@
.then(function (res) {
document.getElementById('adminCurrentUsername').textContent = res && res.logged_in ? '当前用户' : '未登录';
document.getElementById('adminCurrentUserRole').style.display = 'none';
updateShopManageGroupButtonsAccess();
})
.catch(function () {
document.getElementById('adminCurrentUsername').textContent = '当前用户';
document.getElementById('adminCurrentUserRole').style.display = 'none';
updateShopManageGroupButtonsAccess();
});
});
}