Merge master into dev/koko after patrol delete fixes

The merge keeps dev/koko patrol-delete asset fallback behavior while accepting the latest master feature work. Conflicts were limited to ignore rules, local environment/config artifacts, tracked pycache binaries, and the Flask main blueprint.

Constraint: master and dev/koko both edited app/blueprints/main.py around brand and asset serving

Rejected: Drop dev/koko asset fallback logic | patrol delete and rebuilt Vite assets still need hashed asset resolution across app/new_web_source locations

Confidence: medium

Scope-risk: broad

Directive: app/.env remains a tracked local-config file in this repository; do not print or normalize secrets during conflict handling

Tested: uv run python -m py_compile blueprints\\main.py amazon\\base.py amazon\\main.py

Tested: uv run --group dev pytest tests\\test_amazon_base.py tests\\test_patrol_delete.py

Not-tested: Full backend-java/frontend-vue test suites
This commit is contained in:
koko
2026-04-28 20:55:27 +08:00
171 changed files with 8021 additions and 7487 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -475,7 +475,8 @@ class AmzoneApprove(AmamzonBase):
retry_num = 0
already_asin = set()
now_page = None
while retry_num < 3: # 最多重试3次
max_retry_num = 5
while retry_num < max_retry_num: # 最多重试3次
# if num > 3: #测试
# return
# 等待加载完成
@@ -505,8 +506,11 @@ class AmzoneApprove(AmamzonBase):
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=10)
print(f"获取到 {len(sku_ls)}")
if len(sku_ls) == 0:
print(f"搜索不出SKU停止")
break
retry_num += 1
print(f"没有获取到 ASIN 商品,重试{retry_num}/{max_retry_num}")
self.tab.refresh()
self.tab.wait.doc_loaded(raise_err=False, timeout=120)
continue
# for sku_ele in sku_ls[0:2]:
for sku_ele in sku_ls[1:]:
# solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]')

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ from config import runing_task, runing_shop,base_dir,DELETE_BRAND_API_BASE
class RepricingLogic:
@staticmethod
def situation_1_general_decrease(my_price, rank1_price, is_first_time=False):
"""
@@ -27,9 +27,9 @@ class RepricingLogic:
"""
if is_first_time:
return rank1_price - 0.3
diff = abs(my_price - rank1_price)
diff = my_price - rank1_price
# 精简了原图中长串的阶梯逻辑
if diff <= 0.3:
return rank1_price - 0.5
@@ -47,63 +47,63 @@ class RepricingLogic:
情况2已经是自己购物车
逻辑:判断与第二名的差价,如果拉开足够差距,则稍微降一点点(减0.5)保持优势,防止卖太便宜。
"""
diff = abs(rank2_price - my_price)
diff = rank2_price - my_price
# 1. 绝对差价2欧以上
if diff >= 2.0:
return rank2_price - 0.3
# 2. 产品售价区间判定 (是否要提高价格)
if (20 <= my_price < 30 and diff >= 4) or \
(30 <= my_price < 60 and diff >= 8) or \
(60 <= my_price <= 150 and diff >= 15):
(30 <= my_price < 60 and diff >= 8) or \
(60 <= my_price <= 150 and diff >= 15):
return rank2_price - 0.3 # 满足区间大差价,提价至第二名之下
return my_price # 不满足条件则保持原价
return my_price # 不满足条件则保持原价
@staticmethod
def situation_3_not_own_buybox(my_price, rank1_price):
"""
情况3不是自己购物车 (常规情况)
"""
diff = abs(my_price - rank1_price)
diff = my_price - rank1_price
if diff <= 0.3:
return rank1_price - 0.5
elif 0.5 <= diff <= 0.8:
return rank1_price - 0.7
elif 0.8 < diff <= 2.5:
return rank1_price - 1.0
elif diff > 2.5: # 2.5-3.5以上跳过
return None
return rank1_price - 0.3 # 默认按第一名减0.3
elif diff > 2.5: # 2.5-3.5以上跳过
return None
return rank1_price - 0.3 # 默认按第一名减0.3
@staticmethod
def situation_4_encounter_amz_us_1st(my_price, amz_us_price):
"""
情况4第一名是 Amazon US 卖家
"""
diff = abs(my_price - amz_us_price)
diff = my_price - amz_us_price
if diff <= 3:
return amz_us_price - 2
elif 4 <= diff <= 6:
return amz_us_price - 3
elif 8 <= diff <= 12:
return None # 跳过不跟价
return amz_us_price - 3 # 默认直接按照 Amazon US 减去3
return None # 跳过不跟价
return amz_us_price - 3 # 默认直接按照 Amazon US 减去3
@staticmethod
def situation_5_im_1st_amz_us_2nd(my_price, amz_us_price):
"""
情况5自己是第一名第二名是 Amazon US 卖家
"""
diff = abs(amz_us_price - my_price)
diff = amz_us_price - my_price
if diff <= 7:
return None # 相差5以内跳过 (保持原价)
return None # 相差5以内跳过 (保持原价)
else:
return amz_us_price - 5
@@ -127,7 +127,12 @@ def calculate_target_price(
# --- Step 2: 紫鸟后台特殊判定 (最高优先级) ---
if recommended_shipping > 0:
price_1 = float(front_end_data.get("top_sellers")[0].get("price")) # 实际第一名价格
return RepricingLogic.situation_1_general_decrease(my_price,price_1,is_my_buybox)
if is_my_buybox:
price_col = price_1
else:
price_col = recommended_price + recommended_shipping
print(f"【紫鸟后台价格】{is_my_buybox} 推荐价格: {recommended_price},推荐运费: {recommended_shipping},总和: {price_col}")
return RepricingLogic.situation_1_general_decrease(my_price,price_col,is_my_buybox)
# backend_base_price = recommended_price
# backend_shipping = recommended_shipping
# total_price = backend_base_price + backend_shipping
@@ -151,7 +156,7 @@ def calculate_target_price(
# 分支 A第一名是 Amazon US 卖家 (特殊强敌优先)
price_1 = float(front_end_data.get("top_sellers")[0].get("price")) # 实际第一名价格
if "Amazon" in shop_name_1:
return RepricingLogic.isituation_4_encounter_amz_us_1st(my_price,price_1)
return RepricingLogic.situation_4_encounter_amz_us_1st(my_price,price_1)
# 分支 B不是 Amazon US且目前是自己的购物车
@@ -619,6 +624,11 @@ class AmzonePriceMatch(AmamzonBase):
page_pamel = self.tab.eles('xpath://kat-pagination', timeout=5)
if len(page_pamel) > 0:
current_page = page_pamel[0].sr('xpath:.//ul[@class="pages"]//li[@aria-current="true"]').text
#测试=====
# if int(current_page) > 3:
# break
# 测试===========
# yield (None,{"page":int(current_page.strip())})
# 总页数
total_page = page_pamel[0].sr.eles(
@@ -653,9 +663,9 @@ class AmzonePriceMatch(AmamzonBase):
'xpath:.//div[contains(@class,"JanusSplitBox-module__container")]//div[contains(@class,"JanusSplitBox-module__panel--") and contains(string(.),"ASIN")]/..//div[last()]',
timeout=3).text
print(f"{self.mark_name}】ASIN {asin} 找到....")
if asin in already_asin:
print(f"{self.mark_name}{asin} 已经处理过了,跳过")
continue
# if asin in already_asin:
# print(f"【{self.mark_name}】{asin} 已经处理过了,跳过")
# continue
if asin in skip_asin:
yield (asin, {
"statu": "跳过,需要跳过的ASIN"
@@ -779,7 +789,8 @@ class AmzonePriceMatch(AmamzonBase):
"secondPlace": price_2,
"cartShopName" : cartShopName,
"shippingFee" : f"{shipping_fee}",
"priceChangeStatus" : "改价成功",
"priceChangeStatus" : f"{adjust_prices}",
"modifyCount" : "1",
})
else:
recommendedPrice = recommend_price + shipping_fee
@@ -797,7 +808,8 @@ class AmzonePriceMatch(AmamzonBase):
"secondPlace": price_2,
"cartShopName": cartShopName,
"shippingFee": f"{shipping_fee}",
"priceChangeStatus": "跳过,无需改价",
"priceChangeStatus": "",
"modifyCount": "0",
})
already_asin.add(asin)
@@ -1322,30 +1334,35 @@ class PriceTask:
url = f"{DELETE_BRAND_API_BASE}/api/price-track/tasks/{task_id}/result"
country_aliases = {name: code for code, name in self.country_info.items()}
country_key = country_aliases.get(str(country_code).strip(), str(country_code).strip())
countries = {}
if asin or status:
countries[country_key] = [
{
"shopMallName": shopMallName,
"asin": asin,
"price": status.get("currentPrice") if status.get("currentPrice") else "",
"recommendedPrice": status.get("recommendedPrice") if status.get("recommendedPrice") else "",
"minimumPrice": status.get("minimumPrice") if status.get("minimumPrice") else "",
"firstPlace": status.get("firstPlace") if status.get("firstPlace") else "",
"secondPlace": status.get("secondPlace") if status.get("secondPlace") else "",
"cartShopName": status.get("cartShopName") if status.get("cartShopName") else "",
"priceChangeStatus": status.get("priceChangeStatus") if status.get("priceChangeStatus") else "",
"deleteSkipAsin": status.get("deleteSkipAsin", False),
"removeAsin": status.get("removeAsin") if status.get("removeAsin") else "",
"modifyCount": status.get("modifyCount") if status.get("modifyCount") else "",
"shippingFee": status.get("shippingFee") if status.get("shippingFee") else "",
"status": status.get("statu")
}
]
payload = {
"shops": [
{
"shopName": shop_name,
"countries": {
"additionalProperties1": [
{
"shopMallName": shopMallName,
"asin": asin,
"price": status.get("currentPrice") if status.get("currentPrice") else "",
"recommendedPrice": status.get("recommendedPrice") if status.get("recommendedPrice") else "",
"minimumPrice": status.get("minimumPrice") if status.get("minimumPrice") else "",
"firstPlace": status.get("firstPlace") if status.get("firstPlace") else "",
"secondPlace": status.get("secondPlace") if status.get("secondPlace") else "",
"cartShopName": status.get("cartShopName") if status.get("cartShopName") else "",
"priceChangeStatus": "UPDATED",
"deleteSkipAsin": status.get("deleteSkipAsin",False),
"removeAsin": status.get("removeAsin") if status.get("removeAsin") else "",
# "modifyCount": "2",
"shippingFee" : status.get("shippingFee") if status.get("shippingFee") else "",
"status": status.get("statu")
}
]
},
"countries": countries,
"error": ""
}
]
@@ -1391,11 +1408,11 @@ class PriceTask:
if __name__ == '__main__':
# 使用示例
# user_info = {
# "company": "尾号5578的公司115",
# "company": "rongchuang123",
# "username": "自动化_Robot",
# "password": "#20zsg25"
# }
# shop_name = "刘建煌"
# shop_name = "郭亚芳"
# country = "德国"
# kill_process('v6')
# driver = AmzonePriceMatch(user_info)
@@ -1403,8 +1420,8 @@ if __name__ == '__main__':
# sw_suc = driver.SwitchingCountries(country)
# driver.SwitchPage()
# risk_listing_filter = "Active"
# _shopMallName = "Jianhuang 888"
# ap_asin ="B0BGHRP6BS"
# _shopMallName = "yafang123"
# ap_asin ="B0BTHBJDKG"
# skip_asin = []
# for _ in range(3):
# try:
@@ -1422,12 +1439,14 @@ if __name__ == '__main__':
# print(f"ASIN {asin} 的处理结果: {status}")
# print("已完成操作")
front_end_data = {"top_sellers": [{"rank": 1, "price": "36.81", "stock": "5", "shop_name": "Windera"}, {"rank": 2, "price": "36.50", "stock": "100", "shop_name": "Jianhuang 888"}], "cart_seller": "Windera", "timestamp": "2026-04-25 09:32:58"}
current_Price = 36.50
current_shop_name = "Jianhuang 888"
recommended_price = 36.81
recommended_shipping = 0
calculate_target_price(
front_end_data = {'top_sellers': [{'rank': 1, 'price': '50.24', 'stock': '26', 'shop_name': 'yafang123'}, {'rank': 2, 'price': '44.35', 'stock': '100', 'shop_name': 'QinPimy'}], 'cart_seller': 'Amazon US', 'timestamp': '2026-04-27 16:17:42'}
current_Price = 44.35
current_shop_name = "yafang123"
recommended_price = 44.35
recommended_shipping =0.0
res = calculate_target_price(
front_end_data, current_Price, current_shop_name,
recommended_price, recommended_shipping
)
)
print(res)

View File

@@ -1,8 +1,8 @@
"""
主页面蓝图首页、home、图片工作台、品牌页、静态文件、Logo
"""
import os
from flask import Blueprint, send_file, render_template_string
import os
from flask import Blueprint, send_file, render_template_string
from app_common import (
get_db,
@@ -18,8 +18,8 @@ from flask import redirect, url_for, session
from flask import Flask, request, Response, stream_with_context
import requests
from config import base_url,version,JAVA_API_BASE
from config import base_url,version,JAVA_API_BASE
main_bp = Blueprint('main', __name__)
@@ -130,9 +130,9 @@ def brand_page_legacy():
'<header class="top-bar">',
'<header class="top-bar" style="display:none !important;">',
1,
)
content = content.replace('height: calc(100vh - 56px);', 'height: 100vh;', 1)
return render_template_string(content, user_id=session.get('user_id'))
)
content = content.replace('height: calc(100vh - 56px);', 'height: 100vh;', 1)
return render_template_string(content, user_id=session.get('user_id'))
@@ -147,7 +147,7 @@ def serve_static(filename):
return '', 404
return send_file(file_abs, as_attachment=False)
@main_bp.route('/assets/<path:filename>')
def serve_assets(filename):
"""提供 static 目录及子目录下的静态文件访问。"""
@@ -167,25 +167,18 @@ def serve_assets(filename):
return '', 404
@main_bp.route('/new_web_source/<path:filename>')
def serve_new_web_source(filename):
"""提供 new_web_source 目录下的静态页面文件访问。"""
candidate_dirs = [
os.path.abspath(os.path.join(BASE_DIR, 'new_web_source')),
os.path.abspath(os.path.join(BASE_DIR, '..', 'new_web_source')),
]
for static_abs in candidate_dirs:
filepath = os.path.normpath(os.path.join(static_abs, filename))
file_abs = os.path.abspath(filepath)
if not file_abs.startswith(static_abs):
continue
if os.path.isfile(file_abs):
return send_file(file_abs, as_attachment=False)
return '', 404
@main_bp.route('/logo.jpg', methods=['GET'])
def get_logo_image():
@main_bp.route('/new_web_source/<path:filename>')
def serve_new_web_source(filename):
"""提供 static 目录及子目录下的静态文件访问。"""
filepath = os.path.normpath(os.path.join("new_web_source", filename))
static_abs = os.path.abspath("new_web_source")
file_abs = os.path.abspath(filepath)
if not file_abs.startswith(static_abs) or not os.path.isfile(file_abs):
return '', 404
return send_file(file_abs, as_attachment=False)
@main_bp.route('/logo.jpg', methods=['GET'])
def get_logo_image():
return send_file(os.path.join(BASE_DIR, "logo.jpg"), mimetype='image/jpeg')
@@ -253,4 +246,4 @@ def proxy(path):
return Response("无法连接到后端服务", status=502)
except Exception as e:
print(f"代理请求失败: {e}")
return Response(f"代理错误: {str(e)}", status=500)
return Response(f"代理错误: {str(e)}", status=500)

View File

@@ -51,7 +51,7 @@ os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
debug = False
debug = os.getenv("debug", "false").strip().lower() in ("1", "true", "yes", "on")
version = "1.0.56"
APP_UPDATE_URL = f"{_base_url}/api/version/latest" # 检测更新接口地址,返回 { "file_url": "...", "version": "x.x.x" }
os.environ['APP_VERSION'] = version

View File

@@ -50,8 +50,8 @@ class WindowAPI:
def __init__(self, window):
self._window = window
self._is_maximized = False
window.events.maximized += lambda _: setattr(self, '_is_maximized', True)
window.events.restored += lambda _: setattr(self, '_is_maximized', False)
window.events.maximized += lambda *args: setattr(self, '_is_maximized', True)
window.events.restored += lambda *args: setattr(self, '_is_maximized', False)
def close(self):
"""关闭窗口,异步执行清理逻辑"""

View File

@@ -785,7 +785,24 @@
else if (status === 'cancelled') showToast('已取消');
}
function pollRunTask(taskId) {
function verifyTerminalTaskStatus(taskId, expectedStatus) {
return fetch('/api/brand/tasks/' + taskId, {
credentials: 'include',
headers: withUidHeaders({ 'X-Requested-With': 'XMLHttpRequest' })
})
.then(function(r) { return r.json(); })
.then(function(res) {
if (!res.success || !res.task) return null;
var actualStatus = (res.task.status || '').toLowerCase();
if (actualStatus === 'success' || actualStatus === 'failed' || actualStatus === 'cancelled') {
return actualStatus;
}
return null;
})
.catch(function() { return null; });
}
function pollRunTask(taskId) {
pollTaskId = taskId;
var runProgressFill = document.getElementById('runProgressFill');
var runProgressText = document.getElementById('runProgressText');
@@ -848,15 +865,18 @@
break;
}
} catch (err) {}
}
if (terminalStatus) {
try { runTaskEventAbortController.abort(); } catch (e2) {}
runTaskEventAbortController = null;
onRunTaskFinished(terminalStatus);
return;
}
}
}
if (terminalStatus) {
var verifiedStatus = await verifyTerminalTaskStatus(taskId, terminalStatus);
if (verifiedStatus) {
try { runTaskEventAbortController.abort(); } catch (e2) {}
runTaskEventAbortController = null;
onRunTaskFinished(verifiedStatus);
return;
}
}
}
} catch (err) {}
})();
@@ -918,7 +938,7 @@
});
}
tick();
pollTimer = setInterval(tick, 10000);
pollTimer = setInterval(tick, 3000);
}
document.getElementById('btnRun').onclick = function() {
@@ -1128,7 +1148,11 @@
});
loadTasks();
// setInterval(loadTasks, 10000);
setInterval(function() {
if (cachedTasks.some(function(t) { return (t.status || '').toLowerCase() === 'running'; })) {
loadTasks();
}
}, 3000);
if (window.addEventListener) {
window.addEventListener('pywebviewready', function() {