Compare commits
8 Commits
ac07416352
...
4f8cbc3e38
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f8cbc3e38 | ||
|
|
3ffbb2b004 | ||
|
|
34ed15a9bd | ||
|
|
4b6295dd44 | ||
|
|
01116d5607 | ||
|
|
07904a29aa | ||
|
|
8d683a791d | ||
|
|
ef0e0df0ac |
@@ -14,7 +14,6 @@ def upload_file(file_content: bytes, key: str):
|
|||||||
cfg.credentials_provider = credentials_provider
|
cfg.credentials_provider = credentials_provider
|
||||||
cfg.region = region
|
cfg.region = region
|
||||||
cfg.endpoint = endpoint
|
cfg.endpoint = endpoint
|
||||||
cfg.retry_max_attempts = 3
|
|
||||||
client = oss.Client(cfg)
|
client = oss.Client(cfg)
|
||||||
|
|
||||||
result = client.put_object(
|
result = client.put_object(
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class AmzoneMatchAction(AmamzonBase):
|
|||||||
num = 0
|
num = 0
|
||||||
retry_num = 0
|
retry_num = 0
|
||||||
already_asin = set()
|
already_asin = set()
|
||||||
|
get_page_faild = 0
|
||||||
|
|
||||||
while retry_num < 3: # 最多重试3次
|
while retry_num < 3: # 最多重试3次
|
||||||
# if num > 3: #测试
|
# if num > 3: #测试
|
||||||
@@ -117,9 +118,13 @@ class AmzoneMatchAction(AmamzonBase):
|
|||||||
else:
|
else:
|
||||||
total_page = 0
|
total_page = 0
|
||||||
print(f"【{self.mark_name}】当前页码: {current_page} / 总页数: {total_page}")
|
print(f"【{self.mark_name}】当前页码: {current_page} / 总页数: {total_page}")
|
||||||
|
get_page_faild = 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"【{self.mark_name}】获取页码失败", e)
|
print(f"【{self.mark_name}】获取页码失败", e)
|
||||||
|
get_page_faild+= 1
|
||||||
|
if get_page_faild > 2:
|
||||||
|
show_notification(f"【{self.mark_name}】获取页码失败超3次停止任务!")
|
||||||
|
break
|
||||||
|
|
||||||
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=10)
|
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=10)
|
||||||
print(f"【{self.mark_name}】获取到 {len(sku_ls)}")
|
print(f"【{self.mark_name}】获取到 {len(sku_ls)}")
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from config import runing_task, runing_shop
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from amazon.del_brand import AmamzonBase, kill_process
|
from DrissionPage import Chromium, ChromiumOptions
|
||||||
|
|
||||||
|
from amazon.del_brand import AmamzonBase, kill_process
|
||||||
from amazon.tool import show_notification,get_shop_info
|
from amazon.tool import show_notification,get_shop_info
|
||||||
|
|
||||||
|
from config import runing_task, runing_shop,base_dir
|
||||||
|
|
||||||
|
|
||||||
class ChromeAmzone:
|
class ChromeAmzone:
|
||||||
|
|
||||||
mark_name = "亚马逊详情采集"
|
mark_name = "亚马逊详情采集"
|
||||||
country_info = {
|
country_info = {
|
||||||
"英国": {
|
"英国": {
|
||||||
@@ -43,21 +46,46 @@ class ChromeAmzone:
|
|||||||
os.system('taskkill /f /t /im chrome.exe')
|
os.system('taskkill /f /t /im chrome.exe')
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
# 使用 DrissionPage 启动浏览器(使用默认用户数据)
|
|
||||||
from DrissionPage import Chromium, ChromiumOptions
|
|
||||||
print("正在启动Chrome浏览器...")
|
|
||||||
|
|
||||||
|
print("正在启动Chrome浏览器...")
|
||||||
|
# sellersprite_plug_path = os.path.join(base_dir,"app_resource","sellersprite-extension-mv3")
|
||||||
|
sellersprite_plug_path = os.path.join("D:\\私单交付\\maixiang_AI\\","app_resource","sellersprite-extension-mv3")
|
||||||
|
print(sellersprite_plug_path)
|
||||||
# 配置浏览器选项
|
# 配置浏览器选项
|
||||||
co = ChromiumOptions()
|
co = ChromiumOptions()
|
||||||
|
# co.use_system_user_path(on_off=True)
|
||||||
|
co.set_user_data_path(r"D:\私单交付\maixiang_AI\user_data\chrome_data")
|
||||||
|
co.set_local_port(port=19890)
|
||||||
|
co.add_extension(sellersprite_plug_path)
|
||||||
|
# co.set_argument('--disable-features=DisableLoadExtensionCommandLineSwitch')
|
||||||
|
# co.set_argument('--load-extension',sellersprite_plug_path)
|
||||||
|
co.set_browser_path(r'D:\私单交付\maixiang_AI\app_resource\chrome-win\chrome.exe')
|
||||||
|
|
||||||
# 使用系统默认的用户数据目录
|
# 使用系统默认的用户数据目录
|
||||||
# co.set_user_data_path(r'C:\Users\{}\AppData\Local\Google\Chrome\User Data'.format(os.getenv('USERNAME')))
|
# co.set_user_data_path(r'C:\Users\{}\AppData\Local\Google\Chrome\User Data'.format(os.getenv('USERNAME')))
|
||||||
|
|
||||||
self.browser = Chromium(co)
|
self.browser = Chromium(co)
|
||||||
self.tab = self.browser.latest_tab
|
self.tab = self.browser.latest_tab
|
||||||
print("Chrome浏览器启动成功")
|
print("Chrome浏览器启动成功")
|
||||||
|
|
||||||
def accept_cookie(self):
|
def close_init_popup(self):
|
||||||
pass
|
"""
|
||||||
|
关闭所有的初始化弹窗
|
||||||
|
"""
|
||||||
|
self.tab.wait.doc_loaded(timeout=60,raise_err=True)
|
||||||
|
footbar = self.tab.eles('xpath://footer[@class="el-dialog__footer"]',timeout=5)
|
||||||
|
if len(footbar) > 0:
|
||||||
|
do_not_remind = footbar[0].eles('xpath:.//input[@class="el-checkbox__original"]')
|
||||||
|
if len(do_not_remind) > 0:
|
||||||
|
do_not_remind[0].check()
|
||||||
|
resume_immediately = footbar[0].eles('xpath:.//button')
|
||||||
|
if len(resume_immediately) > 0:
|
||||||
|
resume_immediately[0].click()
|
||||||
|
|
||||||
|
self.tab.wait.doc_loaded(timeout=60,raise_err=True)
|
||||||
|
accept_btn = self.tab.eles('xpath://input[@id="sp-cc-accept"]',timeout=10)
|
||||||
|
if len(accept_btn) > 0:
|
||||||
|
accept_btn[0].click()
|
||||||
|
|
||||||
|
|
||||||
def run(self, country, asin):
|
def run(self, country, asin):
|
||||||
"""
|
"""
|
||||||
@@ -99,6 +127,9 @@ class ChromeAmzone:
|
|||||||
print(f"正在检查并设置邮编: {zip_code}")
|
print(f"正在检查并设置邮编: {zip_code}")
|
||||||
self._set_zip_code(zip_code)
|
self._set_zip_code(zip_code)
|
||||||
|
|
||||||
|
self.close_init_popup()
|
||||||
|
|
||||||
|
|
||||||
# 3. 抓取数据
|
# 3. 抓取数据
|
||||||
print("正在抓取商品数据...")
|
print("正在抓取商品数据...")
|
||||||
data = self._scrape_data()
|
data = self._scrape_data()
|
||||||
@@ -317,14 +348,14 @@ if __name__ == '__main__':
|
|||||||
print(f"采集成功: {result1}")
|
print(f"采集成功: {result1}")
|
||||||
|
|
||||||
# 示例2:采集德国站点的商品信息
|
# 示例2:采集德国站点的商品信息
|
||||||
print("\n示例2:采集德国站点商品")
|
# print("\n示例2:采集德国站点商品")
|
||||||
result2 = chrome.run(country="德国", asin="B0CC8CW9G2")
|
# result2 = chrome.run(country="德国", asin="B0CC8CW9G2")
|
||||||
if result2:
|
# if result2:
|
||||||
print(f"采集成功: {result2}")
|
# print(f"采集成功: {result2}")
|
||||||
|
|
||||||
# 关闭浏览器
|
# # 关闭浏览器
|
||||||
print("\n正在关闭浏览器...")
|
# print("\n正在关闭浏览器...")
|
||||||
chrome.close()
|
# chrome.close()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"运行出错: {traceback.format_exc()}")
|
print(f"运行出错: {traceback.format_exc()}")
|
||||||
|
|||||||
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
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -268,6 +268,7 @@ def admin_delete_user(uid):
|
|||||||
def admin_user_column_permissions(uid):
|
def admin_user_column_permissions(uid):
|
||||||
"""获取指定用户的栏目权限列表:当前用户只能查自己,管理员可查任意用户。超级管理员返回全部栏目。"""
|
"""获取指定用户的栏目权限列表:当前用户只能查自己,管理员可查任意用户。超级管理员返回全部栏目。"""
|
||||||
current_uid = session.get('user_id')
|
current_uid = session.get('user_id')
|
||||||
|
menu_type = (request.args.get('menu_type') or '').strip().lower()
|
||||||
if current_uid != uid:
|
if current_uid != uid:
|
||||||
role, _ = _get_current_admin_role()
|
role, _ = _get_current_admin_role()
|
||||||
if not role:
|
if not role:
|
||||||
@@ -277,19 +278,32 @@ def admin_user_column_permissions(uid):
|
|||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
cur.execute("SELECT role FROM users WHERE id = %s", (uid,))
|
cur.execute("SELECT role FROM users WHERE id = %s", (uid,))
|
||||||
user_row = cur.fetchone()
|
user_row = cur.fetchone()
|
||||||
|
valid_menu_type = menu_type if menu_type in ('app', 'admin') else ''
|
||||||
if user_row and (user_row.get('role') or '').strip() == 'super_admin':
|
if user_row and (user_row.get('role') or '').strip() == 'super_admin':
|
||||||
cur.execute("""
|
sql = """
|
||||||
SELECT id, name, column_key, created_at FROM columns ORDER BY id
|
SELECT id, name, column_key, menu_type, route_path, sort_order, created_at
|
||||||
""")
|
FROM columns
|
||||||
|
"""
|
||||||
|
params = []
|
||||||
|
if valid_menu_type:
|
||||||
|
sql += " WHERE menu_type = %s"
|
||||||
|
params.append(valid_menu_type)
|
||||||
|
sql += " ORDER BY sort_order ASC, id ASC"
|
||||||
|
cur.execute(sql, tuple(params))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
else:
|
else:
|
||||||
cur.execute("""
|
sql = """
|
||||||
SELECT c.id, c.name, c.column_key, c.created_at
|
SELECT c.id, c.name, c.column_key, c.menu_type, c.route_path, c.sort_order, c.created_at
|
||||||
FROM columns c
|
FROM columns c
|
||||||
INNER JOIN user_column_permission ucp ON ucp.column_id = c.id
|
INNER JOIN user_column_permission ucp ON ucp.column_id = c.id
|
||||||
WHERE ucp.user_id = %s
|
WHERE ucp.user_id = %s
|
||||||
ORDER BY c.id
|
"""
|
||||||
""", (uid,))
|
params = [uid]
|
||||||
|
if valid_menu_type:
|
||||||
|
sql += " AND c.menu_type = %s"
|
||||||
|
params.append(valid_menu_type)
|
||||||
|
sql += " ORDER BY c.sort_order ASC, c.id ASC"
|
||||||
|
cur.execute(sql, tuple(params))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
items = [
|
items = [
|
||||||
@@ -297,6 +311,9 @@ def admin_user_column_permissions(uid):
|
|||||||
'id': r['id'],
|
'id': r['id'],
|
||||||
'name': r['name'],
|
'name': r['name'],
|
||||||
'column_key': r['column_key'],
|
'column_key': r['column_key'],
|
||||||
|
'menu_type': r.get('menu_type') or 'app',
|
||||||
|
'route_path': r.get('route_path') or '',
|
||||||
|
'sort_order': r.get('sort_order') or 0,
|
||||||
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
|
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
|
||||||
}
|
}
|
||||||
for r in rows
|
for r in rows
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ def wb():
|
|||||||
@main_bp.route('/brand')
|
@main_bp.route('/brand')
|
||||||
@login_required
|
@login_required
|
||||||
def brand_page():
|
def brand_page():
|
||||||
return _render_html('brand.html')
|
return _render_html('brand.html', user_id=session.get('user_id'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
|||||||
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
||||||
|
|
||||||
|
|
||||||
debug = True
|
debug = False
|
||||||
version = "1.0.13"
|
version = "1.0.56"
|
||||||
APP_UPDATE_URL = f"{_base_url}/api/version/latest" # 检测更新接口地址,返回 { "file_url": "...", "version": "x.x.x" }
|
APP_UPDATE_URL = f"{_base_url}/api/version/latest" # 检测更新接口地址,返回 { "file_url": "...", "version": "x.x.x" }
|
||||||
os.environ['APP_VERSION'] = version
|
os.environ['APP_VERSION'] = version
|
||||||
os.environ['APP_UPDATE_URL'] = APP_UPDATE_URL
|
os.environ['APP_UPDATE_URL'] = APP_UPDATE_URL
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ def main():
|
|||||||
window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder,
|
window.expose(api.close, api.minimize, api.maximize, api.toggle_maximize, generate_images, api.save_image, api.save_image_to_folder, api.select_folder, api.select_brand_xlsx_files, api.select_brand_folder,
|
||||||
api.save_file_from_url, api.save_template_xlsx,api.save_template_zip,api.upload_file_to_java,api.open_external_url,api.enqueue_json,api.get_detail_del,api.save_file_from_url_new)
|
api.save_file_from_url, api.save_template_xlsx,api.save_template_zip,api.upload_file_to_java,api.open_external_url,api.enqueue_json,api.get_detail_del,api.save_file_from_url_new)
|
||||||
webview.start(
|
webview.start(
|
||||||
debug=True,
|
debug=False,
|
||||||
storage_path=cache_path,
|
storage_path=cache_path,
|
||||||
private_mode=False
|
private_mode=False
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>格式转换 - 数富AI</title>
|
<title>格式转换 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/convert.js"></script>
|
<script type="module" crossorigin src="/assets/convert.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-DiP0HdY6.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-ClWy2SbE.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-BATjYfEL.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-DBpLCgAF.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-D-mMH8F6.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-hj_EXwAP.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/convert-7wWJ02Tw.css">
|
<link rel="stylesheet" crossorigin href="/assets/convert-7wWJ02Tw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据去重 - 数富AI</title>
|
<title>数据去重 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
<script type="module" crossorigin src="/assets/dedupe.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-DiP0HdY6.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-ClWy2SbE.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-BATjYfEL.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-DBpLCgAF.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-D-mMH8F6.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-hj_EXwAP.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/dedupe-BpNHwt51.css">
|
<link rel="stylesheet" crossorigin href="/assets/dedupe-BpNHwt51.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>删除品牌 - 数富AI</title>
|
<title>删除品牌 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
<script type="module" crossorigin src="/assets/delete-brand.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-DiP0HdY6.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-ClWy2SbE.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-BATjYfEL.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-DBpLCgAF.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-D-mMH8F6.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-hj_EXwAP.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/delete-brand-DfRLfrtm.css">
|
<link rel="stylesheet" crossorigin href="/assets/delete-brand-CdxGWtvK.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>数据拆分 - 数富AI</title>
|
<title>数据拆分 - 数富AI</title>
|
||||||
<script type="module" crossorigin src="/assets/split.js"></script>
|
<script type="module" crossorigin src="/assets/split.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/pywebview-DiP0HdY6.js">
|
<link rel="modulepreload" crossorigin href="/assets/pywebview-ClWy2SbE.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/brand-BATjYfEL.js">
|
<link rel="modulepreload" crossorigin href="/assets/brand-DBpLCgAF.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/pywebview-D-mMH8F6.css">
|
<link rel="stylesheet" crossorigin href="/assets/pywebview-hj_EXwAP.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/split-CRUIYKS6.css">
|
<link rel="stylesheet" crossorigin href="/assets/split-CRUIYKS6.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
0
app/update/PyQt5/Qt/.keep_dir.txt
Normal file
0
app/update/PyQt5/Qt/.keep_dir.txt
Normal file
0
app/update/PyQt5/Qt5/.keep_dir.txt
Normal file
0
app/update/PyQt5/Qt5/.keep_dir.txt
Normal file
BIN
app/update/PyQt5/QtCore.pyd
Normal file
BIN
app/update/PyQt5/QtCore.pyd
Normal file
Binary file not shown.
BIN
app/update/PyQt5/QtGui.pyd
Normal file
BIN
app/update/PyQt5/QtGui.pyd
Normal file
Binary file not shown.
BIN
app/update/PyQt5/QtWidgets.pyd
Normal file
BIN
app/update/PyQt5/QtWidgets.pyd
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/iconengines/qsvgicon.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/iconengines/qsvgicon.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/imageformats/qgif.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/imageformats/qgif.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/imageformats/qicns.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/imageformats/qicns.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/imageformats/qico.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/imageformats/qico.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/imageformats/qjpeg.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/imageformats/qjpeg.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/imageformats/qsvg.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/imageformats/qsvg.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/imageformats/qtga.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/imageformats/qtga.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/imageformats/qtiff.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/imageformats/qtiff.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/imageformats/qwbmp.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/imageformats/qwbmp.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/imageformats/qwebp.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/imageformats/qwebp.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/mediaservice/dsengine.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/mediaservice/dsengine.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/mediaservice/qtmedia_audioengine.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/mediaservice/qtmedia_audioengine.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/mediaservice/wmfengine.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/mediaservice/wmfengine.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/platforms/qminimal.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/platforms/qminimal.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/platforms/qoffscreen.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/platforms/qoffscreen.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/platforms/qwebgl.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/platforms/qwebgl.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/platforms/qwindows.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/platforms/qwindows.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/platformthemes/qxdgdesktopportal.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/platformthemes/qxdgdesktopportal.dll
Normal file
Binary file not shown.
Binary file not shown.
BIN
app/update/PyQt5/qt-plugins/styles/qwindowsvistastyle.dll
Normal file
BIN
app/update/PyQt5/qt-plugins/styles/qwindowsvistastyle.dll
Normal file
Binary file not shown.
BIN
app/update/PyQt5/sip.pyd
Normal file
BIN
app/update/PyQt5/sip.pyd
Normal file
Binary file not shown.
BIN
app/update/_bz2.pyd
Normal file
BIN
app/update/_bz2.pyd
Normal file
Binary file not shown.
BIN
app/update/_ctypes.pyd
Normal file
BIN
app/update/_ctypes.pyd
Normal file
Binary file not shown.
BIN
app/update/_decimal.pyd
Normal file
BIN
app/update/_decimal.pyd
Normal file
Binary file not shown.
BIN
app/update/_elementtree.pyd
Normal file
BIN
app/update/_elementtree.pyd
Normal file
Binary file not shown.
BIN
app/update/_hashlib.pyd
Normal file
BIN
app/update/_hashlib.pyd
Normal file
Binary file not shown.
BIN
app/update/_hashlib.zip
Normal file
BIN
app/update/_hashlib.zip
Normal file
Binary file not shown.
BIN
app/update/_lzma.pyd
Normal file
BIN
app/update/_lzma.pyd
Normal file
Binary file not shown.
BIN
app/update/_socket.pyd
Normal file
BIN
app/update/_socket.pyd
Normal file
Binary file not shown.
BIN
app/update/libcrypto-1_1-x64.dll
Normal file
BIN
app/update/libcrypto-1_1-x64.dll
Normal file
Binary file not shown.
BIN
app/update/libeay32.dll
Normal file
BIN
app/update/libeay32.dll
Normal file
Binary file not shown.
BIN
app/update/libffi-7.dll
Normal file
BIN
app/update/libffi-7.dll
Normal file
Binary file not shown.
BIN
app/update/msvcp140.dll
Normal file
BIN
app/update/msvcp140.dll
Normal file
Binary file not shown.
BIN
app/update/msvcp140_1.dll
Normal file
BIN
app/update/msvcp140_1.dll
Normal file
Binary file not shown.
BIN
app/update/psutil/_psutil_windows.pyd
Normal file
BIN
app/update/psutil/_psutil_windows.pyd
Normal file
Binary file not shown.
BIN
app/update/pyexpat.pyd
Normal file
BIN
app/update/pyexpat.pyd
Normal file
Binary file not shown.
BIN
app/update/python3.dll
Normal file
BIN
app/update/python3.dll
Normal file
Binary file not shown.
BIN
app/update/python39.dll
Normal file
BIN
app/update/python39.dll
Normal file
Binary file not shown.
BIN
app/update/qt5core.dll
Normal file
BIN
app/update/qt5core.dll
Normal file
Binary file not shown.
BIN
app/update/qt5dbus.dll
Normal file
BIN
app/update/qt5dbus.dll
Normal file
Binary file not shown.
BIN
app/update/qt5gui.dll
Normal file
BIN
app/update/qt5gui.dll
Normal file
Binary file not shown.
BIN
app/update/qt5multimedia.dll
Normal file
BIN
app/update/qt5multimedia.dll
Normal file
Binary file not shown.
BIN
app/update/qt5network.dll
Normal file
BIN
app/update/qt5network.dll
Normal file
Binary file not shown.
BIN
app/update/qt5printsupport.dll
Normal file
BIN
app/update/qt5printsupport.dll
Normal file
Binary file not shown.
BIN
app/update/qt5qml.dll
Normal file
BIN
app/update/qt5qml.dll
Normal file
Binary file not shown.
BIN
app/update/qt5qmlmodels.dll
Normal file
BIN
app/update/qt5qmlmodels.dll
Normal file
Binary file not shown.
BIN
app/update/qt5quick.dll
Normal file
BIN
app/update/qt5quick.dll
Normal file
Binary file not shown.
BIN
app/update/qt5svg.dll
Normal file
BIN
app/update/qt5svg.dll
Normal file
Binary file not shown.
BIN
app/update/qt5websockets.dll
Normal file
BIN
app/update/qt5websockets.dll
Normal file
Binary file not shown.
BIN
app/update/qt5widgets.dll
Normal file
BIN
app/update/qt5widgets.dll
Normal file
Binary file not shown.
BIN
app/update/select.pyd
Normal file
BIN
app/update/select.pyd
Normal file
Binary file not shown.
BIN
app/update/ssleay32.dll
Normal file
BIN
app/update/ssleay32.dll
Normal file
Binary file not shown.
BIN
app/update/unicodedata.pyd
Normal file
BIN
app/update/unicodedata.pyd
Normal file
Binary file not shown.
BIN
app/update/update.exe
Normal file
BIN
app/update/update.exe
Normal file
Binary file not shown.
BIN
app/update/vcruntime140.dll
Normal file
BIN
app/update/vcruntime140.dll
Normal file
Binary file not shown.
BIN
app/update/vcruntime140_1.dll
Normal file
BIN
app/update/vcruntime140_1.dll
Normal file
Binary file not shown.
@@ -62,6 +62,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.nav-permissions-loading .nav-section[data-column-key] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-section + .nav-section::before {
|
.nav-section + .nav-section::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -383,7 +387,7 @@
|
|||||||
.toast.show { opacity: 1; }
|
.toast.show { opacity: 1; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body data-user-id="{{ user_id or '' }}" class="nav-permissions-loading">
|
||||||
<header class="top-bar">
|
<header class="top-bar">
|
||||||
<div class="logo-area">
|
<div class="logo-area">
|
||||||
<span class="app-name">数富AI-亚马逊</span>
|
<span class="app-name">数富AI-亚马逊</span>
|
||||||
@@ -391,33 +395,33 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav class="nav-tabs">
|
<nav class="nav-tabs">
|
||||||
<div class="nav-tab-group">
|
<div class="nav-tab-group">
|
||||||
<div class="nav-section">
|
<div class="nav-section" data-column-key="brand_front_tools">
|
||||||
<div class="nav-section-title">前端工具</div>
|
<div class="nav-section-title">前端工具</div>
|
||||||
<div class="nav-section-items">
|
<div class="nav-section-items">
|
||||||
<button type="button" class="nav-item active" data-panel="brandCheck">品牌检测</button>
|
<span class="nav-item disabled">采集数据</span>
|
||||||
<span class="nav-item disabled">采集数据</span>
|
<span class="nav-item disabled">变体分析</span>
|
||||||
<span class="nav-item disabled">变体分析</span>
|
<button type="button" class="nav-item active" data-panel="brandCheck">品牌检测</button>
|
||||||
<a class="nav-item" href="/new_web_source/dedupe.html">数据去重</a>
|
<a class="nav-item" href="/new_web_source/dedupe.html">数据去重</a>
|
||||||
<a class="nav-item" href="/new_web_source/split.html">数据拆分</a>
|
<a class="nav-item" href="/new_web_source/split.html">数据拆分</a>
|
||||||
<a class="nav-item" href="/new_web_source/convert.html">格式转换</a>
|
<a class="nav-item" href="/new_web_source/convert.html">格式转换</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-section">
|
<div class="nav-section" data-column-key="brand_operation_tools">
|
||||||
<div class="nav-section-title">运营工具</div>
|
<div class="nav-section-title">运营工具</div>
|
||||||
<div class="nav-section-items">
|
<div class="nav-section-items">
|
||||||
<a class="nav-item" href="/new_web_source/delete-brand.html">删除ASIN</a>
|
<a class="nav-item" href="/new_web_source/delete-brand.html">删除ASIN</a>
|
||||||
<a class="nav-item" href="/new_web_source/product-risk.html">商品风险解决</a>
|
<a class="nav-item" href="/new_web_source/product-risk.html">商品风险解决</a>
|
||||||
<a class="nav-item" href="/new_web_source/shop-match.html">定时匹配</a>
|
<a class="nav-item" href="/new_web_source/shop-match.html">定时匹配</a>
|
||||||
<span class="nav-item disabled">跟价</span>
|
<a class="nav-item" href="/new_web_source/price-track.html">跟价</a>
|
||||||
<span class="nav-item disabled">巡店删除</span>
|
<span class="nav-item disabled">巡店删除</span>
|
||||||
<span class="nav-item disabled">取款</span>
|
<span class="nav-item disabled">取款</span>
|
||||||
<span class="nav-item disabled">店铺状态查询</span>
|
<span class="nav-item disabled">店铺状态查询</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-section">
|
<div class="nav-section" data-column-key="brand_logistics_tools">
|
||||||
<div class="nav-section-title">后勤工具</div>
|
<div class="nav-section-title">后勤工具</div>
|
||||||
<div class="nav-section-items">
|
<div class="nav-section-items">
|
||||||
<span class="nav-item disabled">采购</span>
|
<span class="nav-item disabled">采购</span>
|
||||||
<span class="nav-item disabled">ERP</span>
|
<span class="nav-item disabled">ERP</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -539,6 +543,10 @@
|
|||||||
var cachedTasks = [];
|
var cachedTasks = [];
|
||||||
var api = window.pywebview && window.pywebview.api;
|
var api = window.pywebview && window.pywebview.api;
|
||||||
|
|
||||||
|
function getAppPermissionCacheKey(uid) {
|
||||||
|
return 'app_column_permissions:' + String(uid || '');
|
||||||
|
}
|
||||||
|
|
||||||
function getUid() {
|
function getUid() {
|
||||||
return localStorage.getItem('uid') || '';
|
return localStorage.getItem('uid') || '';
|
||||||
}
|
}
|
||||||
@@ -562,6 +570,66 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
(function applyNavSectionPermissions() {
|
||||||
|
function finishNavPermissionLoading() {
|
||||||
|
document.body.classList.remove('nav-permissions-loading');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAllNavSections() {
|
||||||
|
document.querySelectorAll('.nav-section[data-column-key]').forEach(function(section) {
|
||||||
|
section.style.display = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var uid = document.body.getAttribute('data-user-id');
|
||||||
|
if (!uid) {
|
||||||
|
showAllNavSections();
|
||||||
|
finishNavPermissionLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem('uid', uid);
|
||||||
|
var cacheKey = getAppPermissionCacheKey(uid);
|
||||||
|
try {
|
||||||
|
var cachedItems = JSON.parse(localStorage.getItem(cacheKey) || 'null');
|
||||||
|
if (Array.isArray(cachedItems)) {
|
||||||
|
showAllNavSections();
|
||||||
|
var cachedKeys = cachedItems.map(function(item) {
|
||||||
|
return (item.column_key || '').toLowerCase();
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.nav-section[data-column-key]').forEach(function(section) {
|
||||||
|
var key = (section.getAttribute('data-column-key') || '').toLowerCase();
|
||||||
|
section.style.display = cachedKeys.indexOf(key) >= 0 ? '' : 'none';
|
||||||
|
});
|
||||||
|
finishNavPermissionLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
fetch('/api/admin/user/' + uid + '/column-permissions?menu_type=app', { credentials: 'same-origin' })
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(res) {
|
||||||
|
showAllNavSections();
|
||||||
|
if (!res || !res.success || !Array.isArray(res.items)) {
|
||||||
|
finishNavPermissionLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(cacheKey, JSON.stringify(res.items));
|
||||||
|
} catch (e) {}
|
||||||
|
var allowedKeys = res.items.map(function(item) {
|
||||||
|
return (item.column_key || '').toLowerCase();
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.nav-section[data-column-key]').forEach(function(section) {
|
||||||
|
var key = (section.getAttribute('data-column-key') || '').toLowerCase();
|
||||||
|
section.style.display = allowedKeys.indexOf(key) >= 0 ? '' : 'none';
|
||||||
|
});
|
||||||
|
finishNavPermissionLoading();
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
showAllNavSections();
|
||||||
|
finishNavPermissionLoading();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
function showToast(msg) {
|
function showToast(msg) {
|
||||||
var el = document.getElementById('toast');
|
var el = document.getElementById('toast');
|
||||||
el.textContent = msg;
|
el.textContent = msg;
|
||||||
|
|||||||
@@ -217,17 +217,24 @@
|
|||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function getAppPermissionCacheKey(uid) {
|
||||||
|
return 'app_column_permissions:' + String(uid || '');
|
||||||
|
}
|
||||||
// 权限校验:根据 column-permissions 接口按 column_key 显示入口
|
// 权限校验:根据 column-permissions 接口按 column_key 显示入口
|
||||||
(function() {
|
(function() {
|
||||||
localStorage.setItem("uid",{{ user_id }})
|
localStorage.setItem("uid",{{ user_id }})
|
||||||
var uid = document.body.getAttribute('data-user-id');
|
var uid = document.body.getAttribute('data-user-id');
|
||||||
if (!uid) return;
|
if (!uid) return;
|
||||||
|
var cacheKey = getAppPermissionCacheKey(uid);
|
||||||
var baseUrl = window.location.origin;
|
var baseUrl = window.location.origin;
|
||||||
var apiUrl = baseUrl + '/api/admin/user/' + uid + '/column-permissions';
|
var apiUrl = baseUrl + '/api/admin/user/' + uid + '/column-permissions';
|
||||||
fetch(apiUrl, { credentials: 'same-origin' })
|
fetch(apiUrl, { credentials: 'same-origin' })
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(res) {
|
.then(function(res) {
|
||||||
if (!res || !res.success || !Array.isArray(res.items)) return;
|
if (!res || !res.success || !Array.isArray(res.items)) return;
|
||||||
|
try {
|
||||||
|
localStorage.setItem(cacheKey, JSON.stringify(res.items));
|
||||||
|
} catch (e) {}
|
||||||
var allowedKeys = res.items.map(function(item) { return (item.column_key || '').toLowerCase(); });
|
var allowedKeys = res.items.map(function(item) { return (item.column_key || '').toLowerCase(); });
|
||||||
document.querySelectorAll('.entrances .entrance-btn[data-column-key]').forEach(function(btn) {
|
document.querySelectorAll('.entrances .entrance-btn[data-column-key]').forEach(function(btn) {
|
||||||
var key = (btn.getAttribute('data-column-key') || '').toLowerCase();
|
var key = (btn.getAttribute('data-column-key') || '').toLowerCase();
|
||||||
@@ -251,6 +258,10 @@
|
|||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
try{
|
try{
|
||||||
localStorage.removeItem('maixiang_api_key');
|
localStorage.removeItem('maixiang_api_key');
|
||||||
|
var uid = document.body.getAttribute('data-user-id');
|
||||||
|
if (uid) {
|
||||||
|
localStorage.removeItem(getAppPermissionCacheKey(uid));
|
||||||
|
}
|
||||||
}catch (e) {
|
}catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,21 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
function clearAppPermissionCaches() {
|
||||||
|
try {
|
||||||
|
var keysToRemove = [];
|
||||||
|
for (var i = 0; i < localStorage.length; i++) {
|
||||||
|
var key = localStorage.key(i);
|
||||||
|
if (key && key.indexOf('app_column_permissions:') === 0) {
|
||||||
|
keysToRemove.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keysToRemove.forEach(function(key) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
fetch('/api/auth/check', { credentials: 'same-origin' })
|
fetch('/api/auth/check', { credentials: 'same-origin' })
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
@@ -142,6 +157,7 @@
|
|||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(res) {
|
.then(function(res) {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
clearAppPermissionCaches();
|
||||||
window.location.href = res.redirect || '/home';
|
window.location.href = res.redirect || '/home';
|
||||||
} else {
|
} else {
|
||||||
var errEl = document.querySelector('.error-msg');
|
var errEl = document.querySelector('.error-msg');
|
||||||
|
|||||||
38
app/winsrc.bat
Normal file
38
app/winsrc.bat
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
echo 正在执行 PowerShell 命令解除文件锁定...
|
||||||
|
powershell -Command "Get-ChildItem -Path \"%cd%\" -Recurse | Unblock-File"
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
echo 文件解锁完成
|
||||||
|
) else (
|
||||||
|
echo 文件解锁失败,请检查 PowerShell 权限
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 检查 ShuFuAi.exe.config 文件...
|
||||||
|
|
||||||
|
if exist "ShuFuAi.exe.config" (
|
||||||
|
echo ShuFuAi.exe.config 已存在,跳过创建
|
||||||
|
) else (
|
||||||
|
echo 正在创建 ShuFuAi.exe.config...
|
||||||
|
(
|
||||||
|
echo ^<?xml version="1.0" encoding="utf-8" ?^>
|
||||||
|
echo ^<configuration^>
|
||||||
|
echo ^<runtime^>
|
||||||
|
echo ^<loadFromRemoteSources enabled="true"/^>
|
||||||
|
echo ^</runtime^>
|
||||||
|
echo ^</configuration^>
|
||||||
|
) > "ShuFuAi.exe.config"
|
||||||
|
|
||||||
|
if exist "ShuFuAi.exe.config" (
|
||||||
|
echo ShuFuAi.exe.config 创建成功
|
||||||
|
) else (
|
||||||
|
echo ShuFuAi.exe.config 创建失败,请检查权限
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 操作完成!
|
||||||
|
pause
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend-java/aiimage-backend.log.2026-04-16.0.gz
Normal file
BIN
backend-java/aiimage-backend.log.2026-04-16.0.gz
Normal file
Binary file not shown.
BIN
backend-java/aiimage-backend.log.2026-04-17.0.gz
Normal file
BIN
backend-java/aiimage-backend.log.2026-04-17.0.gz
Normal file
Binary file not shown.
@@ -1,13 +1,11 @@
|
|||||||
package com.nanri.aiimage.common.api;
|
package com.nanri.aiimage.common.api;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
|
||||||
@Schema(description = "统一响应体")
|
@Schema(description = "统一响应体")
|
||||||
public class ApiResponse<T> {
|
public class ApiResponse<T> {
|
||||||
|
|
||||||
@@ -20,6 +18,20 @@ public class ApiResponse<T> {
|
|||||||
@Schema(description = "响应数据")
|
@Schema(description = "响应数据")
|
||||||
private T data;
|
private T data;
|
||||||
|
|
||||||
|
@Schema(description = "业务错误码,成功时可为空")
|
||||||
|
private Integer code;
|
||||||
|
|
||||||
|
public ApiResponse(boolean success, String message, T data) {
|
||||||
|
this(success, message, data, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponse(boolean success, String message, T data, Integer code) {
|
||||||
|
this.success = success;
|
||||||
|
this.message = message;
|
||||||
|
this.data = data;
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
public static <T> ApiResponse<T> success(T data) {
|
public static <T> ApiResponse<T> success(T data) {
|
||||||
return new ApiResponse<>(true, "操作成功", data);
|
return new ApiResponse<>(true, "操作成功", data);
|
||||||
}
|
}
|
||||||
@@ -31,4 +43,8 @@ public class ApiResponse<T> {
|
|||||||
public static <T> ApiResponse<T> fail(String message) {
|
public static <T> ApiResponse<T> fail(String message) {
|
||||||
return new ApiResponse<>(false, message, null);
|
return new ApiResponse<>(false, message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> fail(Integer code, String message) {
|
||||||
|
return new ApiResponse<>(false, message, null, code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,18 @@ package com.nanri.aiimage.common.exception;
|
|||||||
|
|
||||||
public class BusinessException extends RuntimeException {
|
public class BusinessException extends RuntimeException {
|
||||||
|
|
||||||
|
private final Integer code;
|
||||||
|
|
||||||
public BusinessException(String message) {
|
public BusinessException(String message) {
|
||||||
|
this(null, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BusinessException(Integer code, String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCode() {
|
||||||
|
return code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ public class GlobalExceptionHandler {
|
|||||||
|
|
||||||
@ExceptionHandler(BusinessException.class)
|
@ExceptionHandler(BusinessException.class)
|
||||||
public ApiResponse<Void> handleBusinessException(BusinessException ex) {
|
public ApiResponse<Void> handleBusinessException(BusinessException ex) {
|
||||||
return ApiResponse.fail(ex.getMessage());
|
return ex.getCode() == null
|
||||||
|
? ApiResponse.fail(ex.getMessage())
|
||||||
|
: ApiResponse.fail(ex.getCode(), ex.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.nanri.aiimage.common.util;
|
||||||
|
|
||||||
|
import com.alibaba.excel.EasyExcel;
|
||||||
|
import com.alibaba.excel.context.AnalysisContext;
|
||||||
|
import com.alibaba.excel.event.AnalysisEventListener;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class ExcelStreamReader {
|
||||||
|
|
||||||
|
private ExcelStreamReader() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void readFirstSheet(File file, SheetRowHandler handler) throws IOException {
|
||||||
|
try (InputStream inputStream = new FileInputStream(file)) {
|
||||||
|
readFirstSheet(inputStream, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void readFirstSheet(InputStream inputStream, SheetRowHandler handler) throws IOException {
|
||||||
|
runRead(inputStream, false, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void readAllSheets(File file, SheetRowHandler handler) throws IOException {
|
||||||
|
try (InputStream inputStream = new FileInputStream(file)) {
|
||||||
|
readAllSheets(inputStream, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void readAllSheets(InputStream inputStream, SheetRowHandler handler) throws IOException {
|
||||||
|
runRead(inputStream, true, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runRead(InputStream inputStream, boolean readAllSheets, SheetRowHandler handler) throws IOException {
|
||||||
|
StreamingListener listener = new StreamingListener(handler);
|
||||||
|
try {
|
||||||
|
if (readAllSheets) {
|
||||||
|
EasyExcel.read(inputStream, listener).headRowNumber(1).doReadAll();
|
||||||
|
} else {
|
||||||
|
EasyExcel.read(inputStream, listener).sheet(0).headRowNumber(1).doRead();
|
||||||
|
}
|
||||||
|
} catch (WrappedRuntimeException ex) {
|
||||||
|
if (ex.getCause() instanceof IOException ioException) {
|
||||||
|
throw ioException;
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface SheetRowHandler {
|
||||||
|
default void onHeader(String sheetName, Integer sheetNo, Map<Integer, String> headerMap) throws Exception {
|
||||||
|
}
|
||||||
|
|
||||||
|
void onRow(String sheetName, Integer sheetNo, int rowIndex, Map<Integer, String> headerMap, Map<Integer, String> rowMap) throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StreamingListener extends AnalysisEventListener<Map<Integer, String>> {
|
||||||
|
|
||||||
|
private final SheetRowHandler handler;
|
||||||
|
private Map<Integer, String> currentHeaderMap = Map.of();
|
||||||
|
|
||||||
|
private StreamingListener(SheetRowHandler handler) {
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
|
||||||
|
Map<Integer, String> normalizedHeadMap = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<Integer, String> entry : headMap.entrySet()) {
|
||||||
|
normalizedHeadMap.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
currentHeaderMap = normalizedHeadMap;
|
||||||
|
try {
|
||||||
|
handler.onHeader(sheetName(context), sheetNo(context), currentHeaderMap);
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
throw ex;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new WrappedRuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invoke(Map<Integer, String> data, AnalysisContext context) {
|
||||||
|
Map<Integer, String> normalizedRow = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<Integer, String> entry : data.entrySet()) {
|
||||||
|
normalizedRow.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
handler.onRow(
|
||||||
|
sheetName(context),
|
||||||
|
sheetNo(context),
|
||||||
|
context.readRowHolder().getRowIndex(),
|
||||||
|
currentHeaderMap,
|
||||||
|
normalizedRow
|
||||||
|
);
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
throw ex;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new WrappedRuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sheetName(AnalysisContext context) {
|
||||||
|
return context.readSheetHolder() == null ? "" : context.readSheetHolder().getSheetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer sheetNo(AnalysisContext context) {
|
||||||
|
return context.readSheetHolder() == null ? null : context.readSheetHolder().getSheetNo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class WrappedRuntimeException extends RuntimeException {
|
||||||
|
private WrappedRuntimeException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,18 +18,18 @@ public class DeleteBrandProgressProperties {
|
|||||||
* 商品风险(PRODUCT_RISK_RESOLVE)RUNNING 超时自动收尾/失败,按分钟计算;
|
* 商品风险(PRODUCT_RISK_RESOLVE)RUNNING 超时自动收尾/失败,按分钟计算;
|
||||||
* 与删除品牌共用同一定时调度。
|
* 与删除品牌共用同一定时调度。
|
||||||
*/
|
*/
|
||||||
private long productRiskStaleTimeoutMinutes = 10;
|
private long productRiskStaleTimeoutMinutes = 20;
|
||||||
private long productRiskInitialTimeoutMinutes = 10;
|
private long productRiskInitialTimeoutMinutes = 20;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Price track RUNNING timeout auto-finalize/fail threshold in minutes.
|
* Price track RUNNING timeout auto-finalize/fail threshold in minutes.
|
||||||
*/
|
*/
|
||||||
private long priceTrackStaleTimeoutMinutes = 10;
|
private long priceTrackStaleTimeoutMinutes = 20;
|
||||||
private long priceTrackInitialTimeoutMinutes = 10;
|
private long priceTrackInitialTimeoutMinutes = 20;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shop match RUNNING timeout auto-finalize/fail threshold in minutes.
|
* Shop match RUNNING timeout auto-finalize/fail threshold in minutes.
|
||||||
*/
|
*/
|
||||||
private long shopMatchStaleTimeoutMinutes = 10;
|
private long shopMatchStaleTimeoutMinutes = 20;
|
||||||
private long shopMatchInitialTimeoutMinutes = 10;
|
private long shopMatchInitialTimeoutMinutes = 20;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import com.nanri.aiimage.modules.brand.model.dto.BrandFileAggregateCacheDto;
|
|||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -94,21 +98,28 @@ public class BrandTaskProgressCacheService {
|
|||||||
|
|
||||||
public void saveParsedPayload(Long taskId, Object payload) {
|
public void saveParsedPayload(Long taskId, Object payload) {
|
||||||
try {
|
try {
|
||||||
stringRedisTemplate.opsForValue().set(buildPayloadKey(taskId), objectMapper.writeValueAsString(payload), ttl());
|
Files.createDirectories(buildTaskDir(taskId));
|
||||||
|
Files.writeString(
|
||||||
|
buildPayloadFile(taskId),
|
||||||
|
objectMapper.writeValueAsString(payload),
|
||||||
|
StandardOpenOption.CREATE,
|
||||||
|
StandardOpenOption.TRUNCATE_EXISTING,
|
||||||
|
StandardOpenOption.WRITE
|
||||||
|
);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new BusinessException("暂存品牌原始数据失败");
|
throw new BusinessException("鏆傚瓨鍝佺墝鍘熷鏁版嵁澶辫触");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> T getParsedPayload(Long taskId, TypeReference<T> typeReference) {
|
public <T> T getParsedPayload(Long taskId, TypeReference<T> typeReference) {
|
||||||
String raw = stringRedisTemplate.opsForValue().get(buildPayloadKey(taskId));
|
Path payloadFile = buildPayloadFile(taskId);
|
||||||
if (raw == null || raw.isBlank()) {
|
if (!Files.isRegularFile(payloadFile)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return objectMapper.readValue(raw, typeReference);
|
return objectMapper.readValue(Files.readString(payloadFile), typeReference);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new BusinessException("读取品牌原始数据失败");
|
throw new BusinessException("璇诲彇鍝佺墝鍘熷鏁版嵁澶辫触");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +137,7 @@ public class BrandTaskProgressCacheService {
|
|||||||
int finishedFiles = countCompletedFiles(taskId);
|
int finishedFiles = countCompletedFiles(taskId);
|
||||||
return new ChunkStoreResult(false, false, finishedFiles, aggregate);
|
return new ChunkStoreResult(false, false, finishedFiles, aggregate);
|
||||||
}
|
}
|
||||||
throw new BusinessException("同一分片重复提交但内容不一致: " + fileUrl + "#" + file.getChunkIndex());
|
throw new BusinessException("鍚屼竴鍒嗙墖閲嶅鎻愪氦浣嗗唴瀹逛笉涓€鑷? " + fileUrl + "#" + file.getChunkIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
BrandFileAggregateCacheDto aggregate = getFileAggregate(taskId, fileUrl);
|
BrandFileAggregateCacheDto aggregate = getFileAggregate(taskId, fileUrl);
|
||||||
@@ -174,7 +185,7 @@ public class BrandTaskProgressCacheService {
|
|||||||
try {
|
try {
|
||||||
return objectMapper.readValue(json, BrandFileAggregateCacheDto.class);
|
return objectMapper.readValue(json, BrandFileAggregateCacheDto.class);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new BusinessException("读取品牌文件聚合缓存失败");
|
throw new BusinessException("璇诲彇鍝佺墝鏂囦欢鑱氬悎缂撳瓨澶辫触");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,11 +220,11 @@ public class BrandTaskProgressCacheService {
|
|||||||
|
|
||||||
public void delete(Long taskId) {
|
public void delete(Long taskId) {
|
||||||
stringRedisTemplate.delete(buildKey(taskId));
|
stringRedisTemplate.delete(buildKey(taskId));
|
||||||
stringRedisTemplate.delete(buildPayloadKey(taskId));
|
|
||||||
stringRedisTemplate.delete(buildFileAggregateKey(taskId));
|
stringRedisTemplate.delete(buildFileAggregateKey(taskId));
|
||||||
stringRedisTemplate.delete(buildCompletedFilesKey(taskId));
|
stringRedisTemplate.delete(buildCompletedFilesKey(taskId));
|
||||||
stringRedisTemplate.delete(buildFinalizeLockKey(taskId));
|
stringRedisTemplate.delete(buildFinalizeLockKey(taskId));
|
||||||
stringRedisTemplate.delete(buildLegacyResultKey(taskId));
|
stringRedisTemplate.delete(buildLegacyResultKey(taskId));
|
||||||
|
deleteTaskDirQuietly(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteFileState(Long taskId, String fileUrl) {
|
public void deleteFileState(Long taskId, String fileUrl) {
|
||||||
@@ -236,19 +247,19 @@ public class BrandTaskProgressCacheService {
|
|||||||
stringRedisTemplate.opsForHash().put(buildFileAggregateKey(taskId), normalizeFileUrl(aggregate.getFileUrl()), objectMapper.writeValueAsString(aggregate));
|
stringRedisTemplate.opsForHash().put(buildFileAggregateKey(taskId), normalizeFileUrl(aggregate.getFileUrl()), objectMapper.writeValueAsString(aggregate));
|
||||||
refreshTaskKey(buildFileAggregateKey(taskId));
|
refreshTaskKey(buildFileAggregateKey(taskId));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new BusinessException("暂存品牌文件聚合结果失败");
|
throw new BusinessException("鏆傚瓨鍝佺墝鏂囦欢鑱氬悎缁撴灉澶辫触");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateChunk(BrandCrawlResultFileDto file) {
|
private void validateChunk(BrandCrawlResultFileDto file) {
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
throw new BusinessException("分片不能为空");
|
throw new BusinessException("鍒嗙墖涓嶈兘涓虹┖");
|
||||||
}
|
}
|
||||||
if (file.getChunkIndex() == null || file.getChunkTotal() == null || file.getChunkIndex() <= 0 || file.getChunkTotal() <= 0 || file.getChunkIndex() > file.getChunkTotal()) {
|
if (file.getChunkIndex() == null || file.getChunkTotal() == null || file.getChunkIndex() <= 0 || file.getChunkTotal() <= 0 || file.getChunkIndex() > file.getChunkTotal()) {
|
||||||
throw new BusinessException("分片参数不合法");
|
throw new BusinessException("鍒嗙墖鍙傛暟涓嶅悎娉?");
|
||||||
}
|
}
|
||||||
if (file.getFileUrl() == null || file.getFileUrl().isBlank()) {
|
if (file.getFileUrl() == null || file.getFileUrl().isBlank()) {
|
||||||
throw new BusinessException("fileUrl 不能为空");
|
throw new BusinessException("fileUrl 涓嶈兘涓虹┖");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +281,7 @@ public class BrandTaskProgressCacheService {
|
|||||||
|
|
||||||
private void ensureChunkConsistency(BrandFileAggregateCacheDto aggregate, BrandCrawlResultFileDto file) {
|
private void ensureChunkConsistency(BrandFileAggregateCacheDto aggregate, BrandCrawlResultFileDto file) {
|
||||||
if (aggregate.getChunkTotal() != null && file.getChunkTotal() != null && !aggregate.getChunkTotal().equals(file.getChunkTotal())) {
|
if (aggregate.getChunkTotal() != null && file.getChunkTotal() != null && !aggregate.getChunkTotal().equals(file.getChunkTotal())) {
|
||||||
throw new BusinessException("同一文件的 chunkTotal 不一致: " + aggregate.getFileUrl());
|
throw new BusinessException("鍚屼竴鏂囦欢鐨?chunkTotal 涓嶄竴鑷? " + aggregate.getFileUrl());
|
||||||
}
|
}
|
||||||
if (aggregate.getChunkTotal() == null) {
|
if (aggregate.getChunkTotal() == null) {
|
||||||
aggregate.setChunkTotal(file.getChunkTotal());
|
aggregate.setChunkTotal(file.getChunkTotal());
|
||||||
@@ -322,7 +333,7 @@ public class BrandTaskProgressCacheService {
|
|||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new BusinessException("计算品牌结果分片摘要失败");
|
throw new BusinessException("璁$畻鍝佺墝缁撴灉鍒嗙墖鎽樿澶辫触");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,8 +351,28 @@ public class BrandTaskProgressCacheService {
|
|||||||
return Duration.ofHours(brandProgressProperties.getTtlHours());
|
return Duration.ofHours(brandProgressProperties.getTtlHours());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildPayloadKey(Long taskId) {
|
private Path buildTaskDir(Long taskId) {
|
||||||
return "brand:task:parsed-payload:" + taskId;
|
return Path.of(System.getProperty("java.io.tmpdir"), "brand-task-cache", String.valueOf(taskId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path buildPayloadFile(Long taskId) {
|
||||||
|
return buildTaskDir(taskId).resolve("parsed-payload.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteTaskDirQuietly(Long taskId) {
|
||||||
|
Path taskDir = buildTaskDir(taskId);
|
||||||
|
if (!Files.exists(taskDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (var walk = Files.walk(taskDir)) {
|
||||||
|
walk.sorted(java.util.Comparator.reverseOrder()).forEach(path -> {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildFileAggregateKey(Long taskId) {
|
private String buildFileAggregateKey(Long taskId) {
|
||||||
@@ -383,7 +414,7 @@ public class BrandTaskProgressCacheService {
|
|||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new BusinessException("生成文件缓存键失败");
|
throw new BusinessException("鐢熸垚鏂囦欢缂撳瓨閿け璐?");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ public class BrandTaskService {
|
|||||||
if (deleted == 0) {
|
if (deleted == 0) {
|
||||||
throw new BusinessException("任务不存在或正在执行中无法删除");
|
throw new BusinessException("任务不存在或正在执行中无法删除");
|
||||||
}
|
}
|
||||||
|
brandTaskProgressCacheService.delete(taskId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String resolveDownloadUrl(Long taskId) {
|
public String resolveDownloadUrl(Long taskId) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.nanri.aiimage.common.exception.BusinessException;
|
import com.nanri.aiimage.common.exception.BusinessException;
|
||||||
|
import com.nanri.aiimage.common.util.ExcelStreamReader;
|
||||||
import com.nanri.aiimage.config.StorageProperties;
|
import com.nanri.aiimage.config.StorageProperties;
|
||||||
import com.nanri.aiimage.modules.convert.model.dto.ConvertRunRequest;
|
import com.nanri.aiimage.modules.convert.model.dto.ConvertRunRequest;
|
||||||
import com.nanri.aiimage.modules.convert.model.dto.UploadedSourceFileDto;
|
import com.nanri.aiimage.modules.convert.model.dto.UploadedSourceFileDto;
|
||||||
@@ -19,15 +20,10 @@ import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
|||||||
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
||||||
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
|
||||||
import org.apache.poi.ss.usermodel.Row;
|
|
||||||
import org.apache.poi.ss.usermodel.Sheet;
|
|
||||||
import org.apache.poi.ss.usermodel.Workbook;
|
|
||||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.BufferedWriter;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -242,14 +238,30 @@ public class ConvertRunService {
|
|||||||
|
|
||||||
private List<GeneratedConvertFile> generateOutputFiles(File inputFile, ConvertTemplateEntity templateEntity) throws IOException {
|
private List<GeneratedConvertFile> generateOutputFiles(File inputFile, ConvertTemplateEntity templateEntity) throws IOException {
|
||||||
List<String> outputFilenames = resolveOutputFilenames(templateEntity);
|
List<String> outputFilenames = resolveOutputFilenames(templateEntity);
|
||||||
List<String> rows = buildTxtRows(inputFile, templateEntity);
|
|
||||||
File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "convert-result"));
|
File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "convert-result"));
|
||||||
|
|
||||||
List<GeneratedConvertFile> generatedFiles = new ArrayList<>();
|
List<GeneratedConvertFile> generatedFiles = new ArrayList<>();
|
||||||
|
Map<String, BufferedWriter> writers = new LinkedHashMap<>();
|
||||||
for (String outputFilename : outputFilenames) {
|
for (String outputFilename : outputFilenames) {
|
||||||
File outputFile = buildNamedOutputFile(outputDir, outputFilename);
|
File outputFile = buildNamedOutputFile(outputDir, outputFilename);
|
||||||
FileUtil.writeLines(rows, outputFile, StandardCharsets.UTF_8);
|
|
||||||
generatedFiles.add(new GeneratedConvertFile(outputFilename, outputFile));
|
generatedFiles.add(new GeneratedConvertFile(outputFilename, outputFile));
|
||||||
|
writers.put(outputFilename, Files.newBufferedWriter(outputFile.toPath(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
streamTxtRowsToOutputs(inputFile, templateEntity, writers);
|
||||||
|
} finally {
|
||||||
|
IOException closeException = null;
|
||||||
|
for (BufferedWriter writer : writers.values()) {
|
||||||
|
try {
|
||||||
|
writer.close();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
if (closeException == null) {
|
||||||
|
closeException = ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (closeException != null) {
|
||||||
|
throw closeException;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return generatedFiles;
|
return generatedFiles;
|
||||||
}
|
}
|
||||||
@@ -266,7 +278,9 @@ public class ConvertRunService {
|
|||||||
return List.of(outputFilename);
|
return List.of(outputFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> buildTxtRows(File inputFile, ConvertTemplateEntity templateEntity) throws IOException {
|
private void streamTxtRowsToOutputs(File inputFile,
|
||||||
|
ConvertTemplateEntity templateEntity,
|
||||||
|
Map<String, BufferedWriter> writers) throws IOException {
|
||||||
List<String> requiredColumns = readStringList(templateEntity.getRequiredSourceColumnsJson());
|
List<String> requiredColumns = readStringList(templateEntity.getRequiredSourceColumnsJson());
|
||||||
List<String> headerColumns = readStringList(templateEntity.getHeaderColumnsJson());
|
List<String> headerColumns = readStringList(templateEntity.getHeaderColumnsJson());
|
||||||
Map<String, String> fieldMapping = readStringMap(templateEntity.getFieldMappingJson());
|
Map<String, String> fieldMapping = readStringMap(templateEntity.getFieldMappingJson());
|
||||||
@@ -275,65 +289,60 @@ public class ConvertRunService {
|
|||||||
int blankHeaderRowsAfterSchema = templateEntity.getBlankHeaderRowsAfterSchema() == null
|
int blankHeaderRowsAfterSchema = templateEntity.getBlankHeaderRowsAfterSchema() == null
|
||||||
? 0
|
? 0
|
||||||
: templateEntity.getBlankHeaderRowsAfterSchema();
|
: templateEntity.getBlankHeaderRowsAfterSchema();
|
||||||
|
long batchId = System.currentTimeMillis();
|
||||||
|
int[] rowIndex = new int[]{1};
|
||||||
|
boolean[] headerRead = new boolean[]{false};
|
||||||
|
Map<String, Integer>[] headerMapHolder = new Map[]{null};
|
||||||
|
|
||||||
DataFormatter formatter = new DataFormatter();
|
ExcelStreamReader.readFirstSheet(inputFile, new ExcelStreamReader.SheetRowHandler() {
|
||||||
try (FileInputStream fis = new FileInputStream(inputFile);
|
@Override
|
||||||
Workbook workbook = WorkbookFactory.create(fis)) {
|
public void onHeader(String sheetName, Integer sheetNo, Map<Integer, String> headerMap) throws Exception {
|
||||||
Sheet sheet = workbook.getSheetAt(0);
|
Map<String, Integer> resolvedHeaderMap = new LinkedHashMap<>();
|
||||||
Row headerRow = sheet.getRow(0);
|
for (Map.Entry<Integer, String> entry : headerMap.entrySet()) {
|
||||||
if (headerRow == null) {
|
String value = normalizeCellText(entry.getValue());
|
||||||
throw new BusinessException("Excel header row is empty.");
|
if (!value.isBlank() && !resolvedHeaderMap.containsKey(value)) {
|
||||||
}
|
resolvedHeaderMap.put(value, entry.getKey());
|
||||||
|
|
||||||
Map<String, Integer> headerMap = new LinkedHashMap<>();
|
|
||||||
for (int i = 0; i < headerRow.getLastCellNum(); i++) {
|
|
||||||
String value = normalizeCellText(formatter.formatCellValue(headerRow.getCell(i)));
|
|
||||||
if (!value.isBlank() && !headerMap.containsKey(value)) {
|
|
||||||
headerMap.put(value, i);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> missing = requiredColumns.stream()
|
List<String> missing = requiredColumns.stream()
|
||||||
.filter(column -> !headerMap.containsKey(column))
|
.filter(column -> !resolvedHeaderMap.containsKey(column))
|
||||||
.toList();
|
.toList();
|
||||||
if (!missing.isEmpty()) {
|
if (!missing.isEmpty()) {
|
||||||
throw new BusinessException("Missing required columns: " + String.join(", ", missing));
|
throw new BusinessException("Missing required columns: " + String.join(", ", missing));
|
||||||
}
|
}
|
||||||
|
headerRead[0] = true;
|
||||||
List<String> rows = new ArrayList<>();
|
headerMapHolder[0] = resolvedHeaderMap;
|
||||||
rows.addAll(preambleLines);
|
for (String line : preambleLines) {
|
||||||
rows.add(String.join("\t", headerColumns));
|
writeLineToAll(writers, line);
|
||||||
|
}
|
||||||
|
writeLineToAll(writers, String.join("\t", headerColumns));
|
||||||
for (int i = 0; i < blankHeaderRowsAfterSchema; i++) {
|
for (int i = 0; i < blankHeaderRowsAfterSchema; i++) {
|
||||||
rows.add("\t".repeat(Math.max(0, headerColumns.size() - 1)));
|
writeLineToAll(writers, "\t".repeat(Math.max(0, headerColumns.size() - 1)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
long batchId = System.currentTimeMillis();
|
@Override
|
||||||
int rowIndex = 1;
|
public void onRow(String sheetName, Integer sheetNo, int excelRowIndex, Map<Integer, String> headerMap, Map<Integer, String> rowMap) throws Exception {
|
||||||
int asinIndex = headerMap.getOrDefault("ASIN", -1);
|
Map<String, Integer> resolvedHeaderMap = headerMapHolder[0];
|
||||||
for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
|
if (resolvedHeaderMap == null) {
|
||||||
Row row = sheet.getRow(rowNum);
|
throw new BusinessException("Excel header row is empty.");
|
||||||
if (row == null) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
int asinIndex = resolvedHeaderMap.getOrDefault("ASIN", -1);
|
||||||
String asin = asinIndex >= 0 ? normalizeCellText(formatter.formatCellValue(row.getCell(asinIndex))) : "";
|
String asin = asinIndex >= 0 ? normalizeCellText(rowMap.get(asinIndex)) : "";
|
||||||
if (asin.isBlank()) {
|
if (asin.isBlank()) {
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> lineValues = new ArrayList<>();
|
List<String> lineValues = new ArrayList<>();
|
||||||
for (String column : headerColumns) {
|
for (String column : headerColumns) {
|
||||||
if ("sku".equals(column)) {
|
if ("sku".equals(column)) {
|
||||||
lineValues.add(batchId + "-" + rowIndex);
|
lineValues.add(batchId + "-" + rowIndex[0]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (fieldMapping.containsKey(column)) {
|
if (fieldMapping.containsKey(column)) {
|
||||||
String sourceColumn = fieldMapping.get(column);
|
String sourceColumn = fieldMapping.get(column);
|
||||||
Integer sourceIndex = headerMap.get(sourceColumn);
|
Integer sourceIndex = resolvedHeaderMap.get(sourceColumn);
|
||||||
String value = sourceIndex == null
|
lineValues.add(sourceIndex == null ? "" : normalizeCellText(rowMap.get(sourceIndex)));
|
||||||
? ""
|
|
||||||
: normalizeCellText(formatter.formatCellValue(row.getCell(sourceIndex)));
|
|
||||||
lineValues.add(value);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (defaults.containsKey(column)) {
|
if (defaults.containsKey(column)) {
|
||||||
@@ -343,10 +352,19 @@ public class ConvertRunService {
|
|||||||
lineValues.add("");
|
lineValues.add("");
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.add(String.join("\t", lineValues));
|
writeLineToAll(writers, String.join("\t", lineValues));
|
||||||
rowIndex++;
|
rowIndex[0]++;
|
||||||
}
|
}
|
||||||
return rows;
|
});
|
||||||
|
if (!headerRead[0]) {
|
||||||
|
throw new BusinessException("Excel header row is empty.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeLineToAll(Map<String, BufferedWriter> writers, String line) throws IOException {
|
||||||
|
for (BufferedWriter writer : writers.values()) {
|
||||||
|
writer.write(line);
|
||||||
|
writer.newLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import org.apache.poi.ss.usermodel.DataFormatter;
|
|||||||
import org.apache.poi.ss.usermodel.Row;
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
import org.apache.poi.ss.usermodel.Sheet;
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
import org.apache.poi.ss.usermodel.Workbook;
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -232,7 +232,7 @@ public class DedupeRunService {
|
|||||||
DataFormatter formatter = new DataFormatter();
|
DataFormatter formatter = new DataFormatter();
|
||||||
try (FileInputStream fis = new FileInputStream(inputFile);
|
try (FileInputStream fis = new FileInputStream(inputFile);
|
||||||
Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(fis);
|
Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(fis);
|
||||||
XSSFWorkbook outputWorkbook = new XSSFWorkbook()) {
|
SXSSFWorkbook outputWorkbook = new SXSSFWorkbook(200)) {
|
||||||
Sheet sheet = workbook.getSheetAt(0);
|
Sheet sheet = workbook.getSheetAt(0);
|
||||||
Row headerRow = sheet.getRow(0);
|
Row headerRow = sheet.getRow(0);
|
||||||
if (headerRow == null) {
|
if (headerRow == null) {
|
||||||
@@ -275,7 +275,6 @@ public class DedupeRunService {
|
|||||||
// The preliminary global scan for mainIdHasSubIdMap was removed.
|
// The preliminary global scan for mainIdHasSubIdMap was removed.
|
||||||
// We now determine subset existence contextually (per group) during the main loop.
|
// We now determine subset existence contextually (per group) during the main loop.
|
||||||
}
|
}
|
||||||
List<Row> candidateRows = new ArrayList<>();
|
|
||||||
Set<String> candidateAsinValues = new HashSet<>();
|
Set<String> candidateAsinValues = new HashSet<>();
|
||||||
for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
|
for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
|
||||||
Row row = sheet.getRow(rowNum);
|
Row row = sheet.getRow(rowNum);
|
||||||
@@ -288,7 +287,6 @@ public class DedupeRunService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
candidateRows.add(row);
|
|
||||||
if (asinColumnIndex != null) {
|
if (asinColumnIndex != null) {
|
||||||
String asinValue = dedupeTotalDataService.normalizeComparableValueOrBlank(formatter.formatCellValue(row.getCell(asinColumnIndex)));
|
String asinValue = dedupeTotalDataService.normalizeComparableValueOrBlank(formatter.formatCellValue(row.getCell(asinColumnIndex)));
|
||||||
if (!asinValue.isBlank()) {
|
if (!asinValue.isBlank()) {
|
||||||
@@ -300,7 +298,17 @@ public class DedupeRunService {
|
|||||||
Set<String> matchedAsinValues = dedupeTotalDataService.findExistingComparableValues(candidateAsinValues);
|
Set<String> matchedAsinValues = dedupeTotalDataService.findExistingComparableValues(candidateAsinValues);
|
||||||
Set<String> writtenAsinValues = new HashSet<>();
|
Set<String> writtenAsinValues = new HashSet<>();
|
||||||
int outputRowIndex = 1;
|
int outputRowIndex = 1;
|
||||||
for (Row row : candidateRows) {
|
for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
|
||||||
|
Row row = sheet.getRow(rowNum);
|
||||||
|
if (row == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (idColumnIndex != null) {
|
||||||
|
String idValue = normalizeCellText(formatter.formatCellValue(row.getCell(idColumnIndex)));
|
||||||
|
if (!shouldKeepId(idValue, keepIntegerIds, keepUnderscoreIds, keepIntegerMainIdsWhenNoSubIds, sheet, rowNum, idColumnIndex, formatter)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (asinColumnIndex != null) {
|
if (asinColumnIndex != null) {
|
||||||
String asinValue = dedupeTotalDataService.normalizeComparableValueOrBlank(formatter.formatCellValue(row.getCell(asinColumnIndex)));
|
String asinValue = dedupeTotalDataService.normalizeComparableValueOrBlank(formatter.formatCellValue(row.getCell(asinColumnIndex)));
|
||||||
if (!asinValue.isBlank()) {
|
if (!asinValue.isBlank()) {
|
||||||
@@ -324,6 +332,7 @@ public class DedupeRunService {
|
|||||||
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
|
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
|
||||||
outputWorkbook.write(fos);
|
outputWorkbook.write(fos);
|
||||||
}
|
}
|
||||||
|
outputWorkbook.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.File;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -114,9 +116,9 @@ public class DedupeTotalDataService {
|
|||||||
importProgressMap.put(importId, progress);
|
importProgressMap.put(importId, progress);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] fileBytes = file.getBytes();
|
File tempFile = saveMultipartToTempFile(file);
|
||||||
String filename = file.getOriginalFilename();
|
String filename = file.getOriginalFilename();
|
||||||
Thread.ofVirtual().start(() -> runImportTask(importId, fileBytes, filename));
|
Thread.ofVirtual().start(() -> runImportTask(importId, tempFile, filename));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
importProgressMap.remove(importId);
|
importProgressMap.remove(importId);
|
||||||
throw new BusinessException("读取上传文件失败");
|
throw new BusinessException("读取上传文件失败");
|
||||||
@@ -150,9 +152,9 @@ public class DedupeTotalDataService {
|
|||||||
deleteImportProgressMap.put(importId, progress);
|
deleteImportProgressMap.put(importId, progress);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] fileBytes = file.getBytes();
|
File tempFile = saveMultipartToTempFile(file);
|
||||||
String filename = file.getOriginalFilename();
|
String filename = file.getOriginalFilename();
|
||||||
Thread.ofVirtual().start(() -> runDeleteImportTask(importId, fileBytes, filename));
|
Thread.ofVirtual().start(() -> runDeleteImportTask(importId, tempFile, filename));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
deleteImportProgressMap.remove(importId);
|
deleteImportProgressMap.remove(importId);
|
||||||
throw new BusinessException("读取上传文件失败");
|
throw new BusinessException("读取上传文件失败");
|
||||||
@@ -211,6 +213,74 @@ public class DedupeTotalDataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void runDeleteImportTask(String importId, File tempFile, String filename) {
|
||||||
|
DedupeTotalDataImportProgressVo progress = deleteImportProgressMap.get(importId);
|
||||||
|
if (progress == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
progress.setStatus("running");
|
||||||
|
try (InputStream inputStream = Files.newInputStream(tempFile.toPath())) {
|
||||||
|
DedupeTotalDataImportVo result = deleteFromExcelInternal(inputStream, filename, progress);
|
||||||
|
progress.setTotalRows(result.getTotalRows());
|
||||||
|
progress.setAsinCount(result.getAsinCount());
|
||||||
|
progress.setInsertedCount(result.getInsertedCount());
|
||||||
|
progress.setSkippedCount(result.getSkippedCount());
|
||||||
|
progress.setProcessedRows(result.getTotalRows());
|
||||||
|
progress.setStatus("success");
|
||||||
|
} catch (Exception e) {
|
||||||
|
progress.setStatus("failed");
|
||||||
|
progress.setErrorMessage(e instanceof BusinessException ? e.getMessage() : "鍒犻櫎 Excel 鍖归厤鏁版嵁澶辫触");
|
||||||
|
} finally {
|
||||||
|
deleteQuietly(tempFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runImportTask(String importId, File tempFile, String filename) {
|
||||||
|
DedupeTotalDataImportProgressVo progress = importProgressMap.get(importId);
|
||||||
|
if (progress == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
progress.setStatus("running");
|
||||||
|
try (InputStream inputStream = Files.newInputStream(tempFile.toPath())) {
|
||||||
|
DedupeTotalDataImportVo result = importFromExcelInternal(inputStream, filename, progress);
|
||||||
|
progress.setTotalRows(result.getTotalRows());
|
||||||
|
progress.setAsinCount(result.getAsinCount());
|
||||||
|
progress.setInsertedCount(result.getInsertedCount());
|
||||||
|
progress.setSkippedCount(result.getSkippedCount());
|
||||||
|
progress.setProcessedRows(result.getTotalRows());
|
||||||
|
progress.setStatus("success");
|
||||||
|
} catch (Exception e) {
|
||||||
|
progress.setStatus("failed");
|
||||||
|
progress.setErrorMessage(e instanceof BusinessException ? e.getMessage() : "瀵煎叆 Excel 澶辫触");
|
||||||
|
} finally {
|
||||||
|
deleteQuietly(tempFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File saveMultipartToTempFile(MultipartFile file) throws Exception {
|
||||||
|
String suffix = ".xlsx";
|
||||||
|
String name = file.getOriginalFilename();
|
||||||
|
if (name != null) {
|
||||||
|
int idx = name.lastIndexOf('.');
|
||||||
|
if (idx >= 0) {
|
||||||
|
suffix = name.substring(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File tempFile = File.createTempFile("dedupe-total-data-", suffix);
|
||||||
|
file.transferTo(tempFile);
|
||||||
|
return tempFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteQuietly(File file) {
|
||||||
|
if (file == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(file.toPath());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public DedupeTotalDataImportVo importFromExcel(MultipartFile file) {
|
public DedupeTotalDataImportVo importFromExcel(MultipartFile file) {
|
||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ public class DeleteBrandRunController {
|
|||||||
return ApiResponse.success(deleteBrandRunService.getTaskDetails(request.getTaskIds()));
|
return ApiResponse.success(deleteBrandRunService.getTaskDetails(request.getTaskIds()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/tasks/progress/batch")
|
||||||
|
@Operation(summary = "批量获取删除品牌任务进度摘要", description = "仅返回任务基础状态和行进度摘要,用于前端轮询降载。")
|
||||||
|
public ApiResponse<DeleteBrandTaskBatchVo> getTaskProgressBatch(@Valid @RequestBody DeleteBrandTaskBatchRequest request) {
|
||||||
|
return ApiResponse.success(deleteBrandRunService.getTaskProgress(request.getTaskIds()));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/tasks/{taskId}/deletion-status")
|
@GetMapping("/tasks/{taskId}/deletion-status")
|
||||||
@Operation(summary = "获取删除品牌任务删除状态", description = "供 Python/插件按 taskId 查询该删除品牌任务是否已被前端删空。")
|
@Operation(summary = "获取删除品牌任务删除状态", description = "供 Python/插件按 taskId 查询该删除品牌任务是否已被前端删空。")
|
||||||
public ApiResponse<DeleteBrandTaskDeletionStatusVo> getTaskDeletionStatus(
|
public ApiResponse<DeleteBrandTaskDeletionStatusVo> getTaskDeletionStatus(
|
||||||
|
|||||||
@@ -544,10 +544,87 @@ public class DeleteBrandRunService {
|
|||||||
return vo;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskBatchVo getTaskProgress(java.util.List<Long> taskIds) {
|
||||||
|
com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskBatchVo vo = new com.nanri.aiimage.modules.deletebrand.model.vo.DeleteBrandTaskBatchVo();
|
||||||
|
if (taskIds == null || taskIds.isEmpty()) {
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.List<Long> normalizedTaskIds = taskIds.stream()
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.distinct()
|
||||||
|
.limit(50)
|
||||||
|
.toList();
|
||||||
|
if (normalizedTaskIds.isEmpty()) {
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.Map<Long, FileTaskEntity> cachedTasks = deleteBrandTaskCacheService.getTaskCacheBatch(normalizedTaskIds);
|
||||||
|
java.util.List<Long> missingTaskIds = new java.util.ArrayList<>();
|
||||||
|
for (Long taskId : normalizedTaskIds) {
|
||||||
|
if (!cachedTasks.containsKey(taskId)) {
|
||||||
|
missingTaskIds.add(taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.Map<Long, FileTaskEntity> taskById = new java.util.LinkedHashMap<>(cachedTasks);
|
||||||
|
if (!missingTaskIds.isEmpty()) {
|
||||||
|
java.util.List<FileTaskEntity> dbTasks = fileTaskMapper.selectBatchIds(missingTaskIds).stream()
|
||||||
|
.filter(task -> task != null && MODULE_TYPE.equals(task.getModuleType()))
|
||||||
|
.toList();
|
||||||
|
for (FileTaskEntity dbTask : dbTasks) {
|
||||||
|
taskById.put(dbTask.getId(), dbTask);
|
||||||
|
if ("RUNNING".equals(dbTask.getStatus())) {
|
||||||
|
deleteBrandTaskCacheService.saveTaskCache(dbTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java.util.Map<Long, java.util.Map<Object, Object>> progressByTaskId = deleteBrandTaskCacheService.getProgressBatch(normalizedTaskIds);
|
||||||
|
|
||||||
|
for (Long taskId : normalizedTaskIds) {
|
||||||
|
FileTaskEntity task = taskById.get(taskId);
|
||||||
|
if (task == null) {
|
||||||
|
vo.getMissingTaskIds().add(taskId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
vo.getItems().add(buildTaskProgressDetail(task, progressByTaskId.get(taskId)));
|
||||||
|
}
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
private DeleteBrandTaskDetailVo buildTaskDetail(FileTaskEntity task) {
|
private DeleteBrandTaskDetailVo buildTaskDetail(FileTaskEntity task) {
|
||||||
return buildTaskDetail(task, null);
|
return buildTaskDetail(task, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DeleteBrandTaskDetailVo buildTaskProgressDetail(FileTaskEntity task, java.util.Map<Object, Object> progress) {
|
||||||
|
task = refreshIndexedMatchesIfNeeded(task);
|
||||||
|
DeleteBrandTaskItemVo taskVo = new DeleteBrandTaskItemVo();
|
||||||
|
taskVo.setId(task.getId());
|
||||||
|
taskVo.setTaskNo(task.getTaskNo());
|
||||||
|
taskVo.setStatus(task.getStatus());
|
||||||
|
taskVo.setSourceFileCount(task.getSourceFileCount());
|
||||||
|
taskVo.setSuccessFileCount(task.getSuccessFileCount());
|
||||||
|
taskVo.setFailedFileCount(task.getFailedFileCount());
|
||||||
|
taskVo.setErrorMessage(task.getErrorMessage());
|
||||||
|
taskVo.setCreatedAt(task.getCreatedAt());
|
||||||
|
taskVo.setUpdatedAt(task.getUpdatedAt());
|
||||||
|
taskVo.setFinishedAt(task.getFinishedAt());
|
||||||
|
if ("SUCCESS".equals(task.getStatus())) {
|
||||||
|
taskVo.setDownloadUrl(resolveTaskDownloadUrl(task.getId()));
|
||||||
|
taskVo.setDownloadFilename(resolveTaskDownloadFilename(task.getId()));
|
||||||
|
} else {
|
||||||
|
taskVo.setDownloadUrl(null);
|
||||||
|
taskVo.setDownloadFilename(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteBrandTaskDetailVo detail = new DeleteBrandTaskDetailVo();
|
||||||
|
detail.setTask(taskVo);
|
||||||
|
detail.setLine_progress(buildLineProgress(task.getId(), progress));
|
||||||
|
detail.setFileProgress(java.util.List.of());
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
private DeleteBrandTaskDetailVo buildTaskDetail(FileTaskEntity task, java.util.Map<Object, Object> progress) {
|
private DeleteBrandTaskDetailVo buildTaskDetail(FileTaskEntity task, java.util.Map<Object, Object> progress) {
|
||||||
task = refreshIndexedMatchesIfNeeded(task);
|
task = refreshIndexedMatchesIfNeeded(task);
|
||||||
DeleteBrandTaskItemVo taskVo = new DeleteBrandTaskItemVo();
|
DeleteBrandTaskItemVo taskVo = new DeleteBrandTaskItemVo();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user