Commit fb68a0efdfba85b011ef38ac4e26a56c919b97e0

Authored by tangwang
1 parent 1852e3e3

配置优化

1 1 # Elasticsearch Configuration
2 2 ES_HOST=http://localhost:9200
3   -ES_USERNAME=
4   -ES_PASSWORD=
  3 +ES_USERNAME=essa
  4 +ES_PASSWORD=4hOaLaf41y2VuI8y
5 5  
6 6 # Redis Configuration (Optional)
7 7 REDIS_HOST=localhost
8 8 REDIS_PORT=6479
9   -REDIS_PASSWORD=
  9 +REDIS_PASSWORD=BMfv5aI31kgHWtlx
10 10  
11 11 # DeepL Translation API
12   -DEEPL_AUTH_KEY=
  12 +DEEPL_AUTH_KEY=c9293ab4-ad25-479b-919f-ab4e63b429ed
13 13  
14 14 # Customer Configuration
15 15 CUSTOMER_ID=customer1
... ...
.env.example
1   -# Environment Configuration
  1 +# Environment Configuration Template
2 2 # Copy this file to .env and update with your actual values
3 3  
4 4 # Elasticsearch Configuration (v8.18)
5 5 ES_HOST=http://localhost:9200
6   -ES_USERNAME=essa
7   -ES_PASSWORD=4hOaLaf41y2VuI8y
  6 +ES_USERNAME=
  7 +ES_PASSWORD=
8 8  
9 9 # Redis Configuration (for caching)
10 10 REDIS_HOST=localhost
11 11 REDIS_PORT=6479
12   -REDIS_PASSWORD=BMfv5aI31kgHWtlx
  12 +REDIS_PASSWORD=
13 13  
14 14 # DeepL Translation API
15   -DEEPL_AUTH_KEY=c9293ab4-ad25-479b-919f-ab4e63b429ed
  15 +DEEPL_AUTH_KEY=
16 16  
17 17 # Customer Configuration
18 18 CUSTOMER_ID=customer1
... ... @@ -27,3 +27,10 @@ IMAGE_MODEL_DIR=/data/tw/models/cn-clip
27 27  
28 28 # Cache Directory
29 29 CACHE_DIR=.cache
  30 +
  31 +# MySQL Database Configuration (Shoplazza)
  32 +DB_HOST=
  33 +DB_PORT=3306
  34 +DB_DATABASE=
  35 +DB_USERNAME=
  36 +DB_PASSWORD=
... ...
config/env_config.py
... ... @@ -47,11 +47,11 @@ CACHE_DIR = os.getenv('CACHE_DIR', '.cache')
47 47  
48 48 # MySQL Database Configuration (Shoplazza)
49 49 DB_CONFIG = {
50   - 'host': os.getenv('DB_HOST', '120.79.247.228'),
51   - 'port': int(os.getenv('DB_PORT', 3316)),
52   - 'database': os.getenv('DB_DATABASE', 'saas'),
53   - 'username': os.getenv('DB_USERNAME', 'saas'),
54   - 'password': os.getenv('DB_PASSWORD', 'P89cZHS5d7dFyc9R'),
  50 + 'host': os.getenv('DB_HOST'),
  51 + 'port': int(os.getenv('DB_PORT', 3306)) if os.getenv('DB_PORT') else 3306,
  52 + 'database': os.getenv('DB_DATABASE'),
  53 + 'username': os.getenv('DB_USERNAME'),
  54 + 'password': os.getenv('DB_PASSWORD'),
55 55 }
56 56  
57 57  
... ...
frontend/static/js/app_base.js 0 → 100644
... ... @@ -0,0 +1,576 @@
  1 +// SearchEngine Frontend - Modern UI
  2 +
  3 +const TENANT_ID = '1';
  4 +const API_BASE_URL = 'http://localhost:6002';
  5 +document.getElementById('apiUrl').textContent = API_BASE_URL;
  6 +
  7 +// State Management
  8 +let state = {
  9 + query: '',
  10 + currentPage: 1,
  11 + pageSize: 20,
  12 + totalResults: 0,
  13 + filters: {},
  14 + rangeFilters: {},
  15 + sortBy: '',
  16 + sortOrder: 'desc',
  17 + facets: null,
  18 + lastSearchData: null,
  19 + debug: true // Always enable debug mode for test frontend
  20 +};
  21 +
  22 +// Initialize
  23 +document.addEventListener('DOMContentLoaded', function() {
  24 + console.log('SearchEngine loaded');
  25 + console.log('Debug mode: always enabled (test frontend)');
  26 +
  27 + document.getElementById('searchInput').focus();
  28 +});
  29 +
  30 +// Keyboard handler
  31 +function handleKeyPress(event) {
  32 + if (event.key === 'Enter') {
  33 + performSearch();
  34 + }
  35 +}
  36 +
  37 +// Toggle filters visibility
  38 +function toggleFilters() {
  39 + const filterSection = document.getElementById('filterSection');
  40 + filterSection.classList.toggle('hidden');
  41 +}
  42 +
  43 +// Perform search
  44 +async function performSearch(page = 1) {
  45 + const query = document.getElementById('searchInput').value.trim();
  46 +
  47 + if (!query) {
  48 + alert('Please enter search keywords');
  49 + return;
  50 + }
  51 +
  52 + state.query = query;
  53 + state.currentPage = page;
  54 + state.pageSize = parseInt(document.getElementById('resultSize').value);
  55 +
  56 + const from = (page - 1) * state.pageSize;
  57 +
  58 + // Define facets (简化配置)
  59 + const facets = [
  60 + {
  61 + "field": "category_keyword",
  62 + "size": 15,
  63 + "type": "terms"
  64 + },
  65 + {
  66 + "field": "vendor_keyword",
  67 + "size": 15,
  68 + "type": "terms"
  69 + },
  70 + {
  71 + "field": "tags_keyword",
  72 + "size": 10,
  73 + "type": "terms"
  74 + },
  75 + {
  76 + "field": "min_price",
  77 + "type": "range",
  78 + "ranges": [
  79 + {"key": "0-50", "to": 50},
  80 + {"key": "50-100", "from": 50, "to": 100},
  81 + {"key": "100-200", "from": 100, "to": 200},
  82 + {"key": "200+", "from": 200}
  83 + ]
  84 + }
  85 + ];
  86 +
  87 + // Show loading
  88 + document.getElementById('loading').style.display = 'block';
  89 + document.getElementById('productGrid').innerHTML = '';
  90 +
  91 + try {
  92 + const response = await fetch(`${API_BASE_URL}/search/`, {
  93 + method: 'POST',
  94 + headers: {
  95 + 'Content-Type': 'application/json',
  96 + 'X-Tenant-ID': TENANT_ID,
  97 + },
  98 + body: JSON.stringify({
  99 + query: query,
  100 + size: state.pageSize,
  101 + from: from,
  102 + filters: Object.keys(state.filters).length > 0 ? state.filters : null,
  103 + range_filters: Object.keys(state.rangeFilters).length > 0 ? state.rangeFilters : null,
  104 + facets: facets,
  105 + sort_by: state.sortBy || null,
  106 + sort_order: state.sortOrder,
  107 + debug: state.debug
  108 + })
  109 + });
  110 +
  111 + if (!response.ok) {
  112 + throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  113 + }
  114 +
  115 + const data = await response.json();
  116 + state.lastSearchData = data;
  117 + state.totalResults = data.total;
  118 + state.facets = data.facets;
  119 +
  120 + displayResults(data);
  121 + displayFacets(data.facets);
  122 + displayPagination();
  123 + displayDebugInfo(data);
  124 + updateProductCount(data.total);
  125 + updateClearFiltersButton();
  126 +
  127 + } catch (error) {
  128 + console.error('Search error:', error);
  129 + document.getElementById('productGrid').innerHTML = `
  130 + <div class="error-message">
  131 + <strong>Search Error:</strong> ${error.message}
  132 + <br><br>
  133 + <small>Please ensure backend service is running (${API_BASE_URL})</small>
  134 + </div>
  135 + `;
  136 + } finally {
  137 + document.getElementById('loading').style.display = 'none';
  138 + }
  139 +}
  140 +
  141 +// Display results in grid
  142 +function displayResults(data) {
  143 + const grid = document.getElementById('productGrid');
  144 +
  145 + if (!data.results || data.results.length === 0) {
  146 + grid.innerHTML = `
  147 + <div class="no-results" style="grid-column: 1 / -1;">
  148 + <h3>No Results Found</h3>
  149 + <p>Try different keywords or filters</p>
  150 + </div>
  151 + `;
  152 + return;
  153 + }
  154 +
  155 + let html = '';
  156 +
  157 + data.results.forEach((hit) => {
  158 + const score = product.relevance_score;
  159 + html += `
  160 + <div class="product-card">
  161 + <div class="product-image-wrapper">
  162 + ${product.image_url ? `
  163 + <img src="${escapeHtml(product.image_url)}"
  164 + alt="${escapeHtml(product.title)}"
  165 + class="product-image"
  166 + onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22100%22 height=%22100%22%3E%3Crect fill=%22%23f0f0f0%22 width=%22100%22 height=%22100%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 font-size=%2214%22 text-anchor=%22middle%22 dy=%22.3em%22 fill=%22%23999%22%3ENo Image%3C/text%3E%3C/svg%3E'">
  167 + ` : `
  168 + <div style="color: #ccc; font-size: 14px;">No Image</div>
  169 + `}
  170 + </div>
  171 +
  172 + <div class="product-price">
  173 + ${product.price ? `$${product.price.toFixed(2)}` : 'N/A'}${product.compare_at_price && product.compare_at_price > product.price ? `<span style="text-decoration: line-through; color: #999; font-size: 0.9em; margin-left: 8px;">$${product.compare_at_price.toFixed(2)}</span>` : \'\'}
  174 + </div>
  175 +
  176 + <div class="product-stock">
  177 + ${product.in_stock ? '<span style="color: green;">In Stock</span>' : '<span style="color: red;">Out of Stock</span>'}
  178 + ${product.variants && product.variants.length > 0 ? `<span style="color: #666; font-size: 0.9em;">(${product.variants.length} variants)</span>` : ''}
  179 + </div>
  180 +
  181 + <div class="product-title">
  182 + ${escapeHtml(product.title || product.title || 'N/A')}
  183 + </div>
  184 +
  185 + <div class="product-meta">${product.vendor ? escapeHtml(product.vendor) : ''}${product.product_type ? ' | ' + escapeHtml(product.product_type) : ''}${product.category ? ' | ' + escapeHtml(product.category) : ''} ${product.tags ? `
  186 + <div class="product-tags">
  187 + Tags: ${escapeHtml(product.tags)}
  188 + </div>
  189 + ` : ''}
  190 + </div>
  191 + `;
  192 + });
  193 +
  194 + grid.innerHTML = html;
  195 +}
  196 +
  197 +// Display facets as filter tags (重构版 - 标准化格式)
  198 +function displayFacets(facets) {
  199 + if (!facets) return;
  200 +
  201 + facets.forEach(facet => {
  202 + // 根据字段名找到对应的容器
  203 + let containerId = null;
  204 + let maxDisplay = 10;
  205 +
  206 + if (facet.field === 'category_keyword') {
  207 + containerId = 'categoryTags';
  208 + maxDisplay = 10;
  209 + } else if (facet.field === 'vendor_keyword') {
  210 + containerId = 'brandTags';
  211 + maxDisplay = 10;
  212 + } else if (facet.field === 'tags_keyword') {
  213 + containerId = 'supplierTags';
  214 + maxDisplay = 8;
  215 + }
  216 +
  217 + if (!containerId) return;
  218 +
  219 + const container = document.getElementById(containerId);
  220 + if (!container) return;
  221 +
  222 + let html = '';
  223 +
  224 + // 渲染分面值
  225 + facet.values.slice(0, maxDisplay).forEach(facetValue => {
  226 + const value = facetValue.value;
  227 + const count = facetValue.count;
  228 + const selected = facetValue.selected;
  229 +
  230 + html += `
  231 + <span class="filter-tag ${selected ? 'active' : ''}"
  232 + onclick="toggleFilter('${escapeAttr(facet.field)}', '${escapeAttr(value)}')">
  233 + ${escapeHtml(value)} (${count})
  234 + </span>
  235 + `;
  236 + });
  237 +
  238 + container.innerHTML = html;
  239 + });
  240 +}
  241 +
  242 +// Toggle filter
  243 +function toggleFilter(field, value) {
  244 + if (!state.filters[field]) {
  245 + state.filters[field] = [];
  246 + }
  247 +
  248 + const index = state.filters[field].indexOf(value);
  249 + if (index > -1) {
  250 + state.filters[field].splice(index, 1);
  251 + if (state.filters[field].length === 0) {
  252 + delete state.filters[field];
  253 + }
  254 + } else {
  255 + state.filters[field].push(value);
  256 + }
  257 +
  258 + performSearch(1); // Reset to page 1
  259 +}
  260 +
  261 +// Handle price filter (重构版 - 使用 rangeFilters)
  262 +function handlePriceFilter(value) {
  263 + if (!value) {
  264 + delete state.rangeFilters.price;
  265 + } else {
  266 + const priceRanges = {
  267 + '0-50': { lt: 50 },
  268 + '50-100': { gte: 50, lt: 100 },
  269 + '100-200': { gte: 100, lt: 200 },
  270 + '200+': { gte: 200 }
  271 + };
  272 +
  273 + if (priceRanges[value]) {
  274 + state.rangeFilters.price = priceRanges[value];
  275 + }
  276 + }
  277 +
  278 + performSearch(1);
  279 +}
  280 +
  281 +// Handle time filter (重构版 - 使用 rangeFilters)
  282 +function handleTimeFilter(value) {
  283 + if (!value) {
  284 + delete state.rangeFilters.create_time;
  285 + } else {
  286 + const now = new Date();
  287 + let fromDate;
  288 +
  289 + switch(value) {
  290 + case 'today':
  291 + fromDate = new Date(now.setHours(0, 0, 0, 0));
  292 + break;
  293 + case 'week':
  294 + fromDate = new Date(now.setDate(now.getDate() - 7));
  295 + break;
  296 + case 'month':
  297 + fromDate = new Date(now.setMonth(now.getMonth() - 1));
  298 + break;
  299 + case '3months':
  300 + fromDate = new Date(now.setMonth(now.getMonth() - 3));
  301 + break;
  302 + case '6months':
  303 + fromDate = new Date(now.setMonth(now.getMonth() - 6));
  304 + break;
  305 + }
  306 +
  307 + if (fromDate) {
  308 + state.rangeFilters.create_time = {
  309 + gte: fromDate.toISOString()
  310 + };
  311 + }
  312 + }
  313 +
  314 + performSearch(1);
  315 +}
  316 +
  317 +// Clear all filters
  318 +function clearAllFilters() {
  319 + state.filters = {};
  320 + state.rangeFilters = {};
  321 + document.getElementById('priceFilter').value = '';
  322 + document.getElementById('timeFilter').value = '';
  323 + performSearch(1);
  324 +}
  325 +
  326 +// Update clear filters button visibility
  327 +function updateClearFiltersButton() {
  328 + const btn = document.getElementById('clearFiltersBtn');
  329 + if (Object.keys(state.filters).length > 0 || Object.keys(state.rangeFilters).length > 0) {
  330 + btn.style.display = 'inline-block';
  331 + } else {
  332 + btn.style.display = 'none';
  333 + }
  334 +}
  335 +
  336 +// Update product count
  337 +function updateProductCount(total) {
  338 + document.getElementById('productCount').textContent = `${total.toLocaleString()} products found`;
  339 +}
  340 +
  341 +// Sort functions
  342 +function setSortByDefault() {
  343 + // Remove active from all buttons and arrows
  344 + document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
  345 + document.querySelectorAll('.arrow-up, .arrow-down').forEach(a => a.classList.remove('active'));
  346 +
  347 + // Set default button active
  348 + const defaultBtn = document.querySelector('.sort-btn[data-sort=""]');
  349 + if (defaultBtn) defaultBtn.classList.add('active');
  350 +
  351 + state.sortBy = '';
  352 + state.sortOrder = 'desc';
  353 +
  354 + performSearch(1);
  355 +}
  356 +
  357 +function sortByField(field, order) {
  358 + state.sortBy = field;
  359 + state.sortOrder = order;
  360 +
  361 + // Remove active from all buttons (but keep "By default" if no sort)
  362 + document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
  363 +
  364 + // Remove active from all arrows
  365 + document.querySelectorAll('.arrow-up, .arrow-down').forEach(a => a.classList.remove('active'));
  366 +
  367 + // Add active to clicked arrow
  368 + const activeArrow = document.querySelector(`.arrow-up[data-field="${field}"][data-order="${order}"], .arrow-down[data-field="${field}"][data-order="${order}"]`);
  369 + if (activeArrow) {
  370 + activeArrow.classList.add('active');
  371 + }
  372 +
  373 + performSearch(state.currentPage);
  374 +}
  375 +
  376 +// Pagination
  377 +function displayPagination() {
  378 + const paginationDiv = document.getElementById('pagination');
  379 +
  380 + if (state.totalResults <= state.pageSize) {
  381 + paginationDiv.style.display = 'none';
  382 + return;
  383 + }
  384 +
  385 + paginationDiv.style.display = 'flex';
  386 +
  387 + const totalPages = Math.ceil(state.totalResults / state.pageSize);
  388 + const currentPage = state.currentPage;
  389 +
  390 + let html = `
  391 + <button class="page-btn" onclick="goToPage(${currentPage - 1})"
  392 + ${currentPage === 1 ? 'disabled' : ''}>
  393 + ← Previous
  394 + </button>
  395 + `;
  396 +
  397 + // Page numbers
  398 + const maxVisible = 5;
  399 + let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2));
  400 + let endPage = Math.min(totalPages, startPage + maxVisible - 1);
  401 +
  402 + if (endPage - startPage < maxVisible - 1) {
  403 + startPage = Math.max(1, endPage - maxVisible + 1);
  404 + }
  405 +
  406 + if (startPage > 1) {
  407 + html += `<button class="page-btn" onclick="goToPage(1)">1</button>`;
  408 + if (startPage > 2) {
  409 + html += `<span class="page-info">...</span>`;
  410 + }
  411 + }
  412 +
  413 + for (let i = startPage; i <= endPage; i++) {
  414 + html += `
  415 + <button class="page-btn ${i === currentPage ? 'active' : ''}"
  416 + onclick="goToPage(${i})">
  417 + ${i}
  418 + </button>
  419 + `;
  420 + }
  421 +
  422 + if (endPage < totalPages) {
  423 + if (endPage < totalPages - 1) {
  424 + html += `<span class="page-info">...</span>`;
  425 + }
  426 + html += `<button class="page-btn" onclick="goToPage(${totalPages})">${totalPages}</button>`;
  427 + }
  428 +
  429 + html += `
  430 + <button class="page-btn" onclick="goToPage(${currentPage + 1})"
  431 + ${currentPage === totalPages ? 'disabled' : ''}>
  432 + Next →
  433 + </button>
  434 + `;
  435 +
  436 + html += `
  437 + <span class="page-info">
  438 + Page ${currentPage} of ${totalPages} (${state.totalResults.toLocaleString()} results)
  439 + </span>
  440 + `;
  441 +
  442 + paginationDiv.innerHTML = html;
  443 +}
  444 +
  445 +function goToPage(page) {
  446 + const totalPages = Math.ceil(state.totalResults / state.pageSize);
  447 + if (page < 1 || page > totalPages) return;
  448 +
  449 + performSearch(page);
  450 +
  451 + // Scroll to top
  452 + window.scrollTo({ top: 0, behavior: 'smooth' });
  453 +}
  454 +
  455 +// Display debug info
  456 +function displayDebugInfo(data) {
  457 + const debugInfoDiv = document.getElementById('debugInfo');
  458 +
  459 + if (!state.debug || !data.debug_info) {
  460 + // If debug mode is off or no debug info, show basic query info
  461 + if (data.query_info) {
  462 + let html = '<div style="padding: 10px;">';
  463 + html += `<div><strong>查询:</strong> ${escapeHtml(data.query_info.original_query || 'N/A')}</div>`;
  464 + html += `<div><strong>语言:</strong> ${getLanguageName(data.query_info.detected_language)}</div>`;
  465 + html += '</div>';
  466 + debugInfoDiv.innerHTML = html;
  467 + } else {
  468 + debugInfoDiv.innerHTML = '';
  469 + }
  470 + return;
  471 + }
  472 +
  473 + // Display comprehensive debug info when debug mode is on
  474 + const debugInfo = data.debug_info;
  475 + let html = '<div style="padding: 10px; font-family: monospace; font-size: 12px;">';
  476 +
  477 + // Query Analysis
  478 + if (debugInfo.query_analysis) {
  479 + html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">查询分析:</strong>';
  480 + html += `<div>原始查询: ${escapeHtml(debugInfo.query_analysis.original_query || 'N/A')}</div>`;
  481 + html += `<div>标准化: ${escapeHtml(debugInfo.query_analysis.normalized_query || 'N/A')}</div>`;
  482 + html += `<div>重写后: ${escapeHtml(debugInfo.query_analysis.rewritten_query || 'N/A')}</div>`;
  483 + html += `<div>检测语言: ${getLanguageName(debugInfo.query_analysis.detected_language)}</div>`;
  484 + html += `<div>域: ${escapeHtml(debugInfo.query_analysis.domain || 'default')}</div>`;
  485 + html += `<div>简单查询: ${debugInfo.query_analysis.is_simple_query ? '是' : '否'}</div>`;
  486 +
  487 + if (debugInfo.query_analysis.translations && Object.keys(debugInfo.query_analysis.translations).length > 0) {
  488 + html += '<div>翻译: ';
  489 + for (const [lang, translation] of Object.entries(debugInfo.query_analysis.translations)) {
  490 + if (translation) {
  491 + html += `${getLanguageName(lang)}: ${escapeHtml(translation)}; `;
  492 + }
  493 + }
  494 + html += '</div>';
  495 + }
  496 +
  497 + if (debugInfo.query_analysis.boolean_ast) {
  498 + html += `<div>布尔AST: ${escapeHtml(debugInfo.query_analysis.boolean_ast)}</div>`;
  499 + }
  500 +
  501 + html += `<div>向量嵌入: ${debugInfo.query_analysis.has_vector ? '已启用' : '未启用'}</div>`;
  502 + html += '</div>';
  503 + }
  504 +
  505 + // Feature Flags
  506 + if (debugInfo.feature_flags) {
  507 + html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">功能开关:</strong>';
  508 + html += `<div>翻译: ${debugInfo.feature_flags.translation_enabled ? '启用' : '禁用'}</div>`;
  509 + html += `<div>嵌入: ${debugInfo.feature_flags.embedding_enabled ? '启用' : '禁用'}</div>`;
  510 + html += `<div>重排序: ${debugInfo.feature_flags.rerank_enabled ? '启用' : '禁用'}</div>`;
  511 + html += '</div>';
  512 + }
  513 +
  514 + // ES Response
  515 + if (debugInfo.es_response) {
  516 + html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">ES响应:</strong>';
  517 + html += `<div>耗时: ${debugInfo.es_response.took_ms}ms</div>`;
  518 + html += `<div>总命中数: ${debugInfo.es_response.total_hits}</div>`;
  519 + html += `<div>最高分: ${debugInfo.es_response.max_score?.toFixed(3) || 0}</div>`;
  520 + html += '</div>';
  521 + }
  522 +
  523 + // Stage Timings
  524 + if (debugInfo.stage_timings) {
  525 + html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">各阶段耗时:</strong>';
  526 + for (const [stage, duration] of Object.entries(debugInfo.stage_timings)) {
  527 + html += `<div>${stage}: ${duration.toFixed(2)}ms</div>`;
  528 + }
  529 + html += '</div>';
  530 + }
  531 +
  532 + // ES Query
  533 + if (debugInfo.es_query) {
  534 + html += '<div style="margin-bottom: 15px;"><strong style="font-size: 14px;">ES查询DSL:</strong>';
  535 + html += `<pre style="background: #f5f5f5; padding: 10px; overflow: auto; max-height: 400px;">${escapeHtml(JSON.stringify(debugInfo.es_query, null, 2))}</pre>`;
  536 + html += '</div>';
  537 + }
  538 +
  539 + html += '</div>';
  540 + debugInfoDiv.innerHTML = html;
  541 +}
  542 +
  543 +// Helper functions
  544 +function escapeHtml(text) {
  545 + if (!text) return '';
  546 + const div = document.createElement('div');
  547 + div.textContent = text;
  548 + return div.innerHTML;
  549 +}
  550 +
  551 +function escapeAttr(text) {
  552 + if (!text) return '';
  553 + return text.replace(/'/g, "\\'").replace(/"/g, '&quot;');
  554 +}
  555 +
  556 +function formatDate(dateStr) {
  557 + if (!dateStr) return '';
  558 + try {
  559 + const date = new Date(dateStr);
  560 + return date.toLocaleDateString('zh-CN');
  561 + } catch {
  562 + return dateStr;
  563 + }
  564 +}
  565 +
  566 +function getLanguageName(code) {
  567 + const names = {
  568 + 'zh': '中文',
  569 + 'en': 'English',
  570 + 'ru': 'Русский',
  571 + 'ar': 'العربية',
  572 + 'ja': '日本語',
  573 + 'unknown': 'Unknown'
  574 + };
  575 + return names[code] || code;
  576 +}
... ...
scripts/demo_base.sh
... ... @@ -100,7 +100,7 @@ echo &quot;后端服务将在后台运行...&quot;
100 100 mkdir -p logs
101 101  
102 102 # 启动后端(后台运行)
103   -nohup python -m api.app \
  103 +nohup python api/app.py \
104 104 --host 0.0.0.0 \
105 105 --port "$API_PORT" \
106 106 --customer base \
... ...
scripts/generate_test_data.py
... ... @@ -200,7 +200,7 @@ def generate_sku_data(spus: list, start_sku_id: int = 1):
200 200 'image_src': '',
201 201 'wholesale_price': '[{"price": ' + str(round(price * 0.8, 2)) + ', "minQuantity": 10}]',
202 202 'note': '',
203   - 'extend': '',
  203 + 'extend': None, # JSON field, use NULL instead of empty string
204 204 'shoplazza_created_at': created_at.strftime('%Y-%m-%d %H:%M:%S'),
205 205 'shoplazza_updated_at': updated_at.strftime('%Y-%m-%d %H:%M:%S'),
206 206 'tenant_id': spu['tenant_id'],
... ... @@ -216,6 +216,21 @@ def generate_sku_data(spus: list, start_sku_id: int = 1):
216 216 return skus
217 217  
218 218  
  219 +def escape_sql_string(value: str) -> str:
  220 + """
  221 + Escape SQL string value (replace single quotes with doubled quotes).
  222 +
  223 + Args:
  224 + value: String value to escape
  225 +
  226 + Returns:
  227 + Escaped string
  228 + """
  229 + if value is None:
  230 + return ''
  231 + return str(value).replace("'", "''").replace("\\", "\\\\")
  232 +
  233 +
219 234 def generate_sql_inserts(spus: list, skus: list, output_file: str):
220 235 """
221 236 Generate SQL INSERT statements.
... ... @@ -241,21 +256,26 @@ def generate_sql_inserts(spus: list, skus: list, output_file: str):
241 256  
242 257 for i, spu in enumerate(spus):
243 258 values = (
244   - f"({spu['id']}, {spu['shop_id']}, '{spu['shoplazza_id']}', "
245   - f"'{spu['handle']}', '{spu['title']}', '{spu['brief']}', "
246   - f"'{spu['description']}', '{spu['spu']}', '{spu['vendor']}', "
247   - f"'{spu['vendor_url']}', '{spu['seo_title']}', '{spu['seo_description']}', "
248   - f"'{spu['seo_keywords']}', '{spu['image_src']}', {spu['image_width']}, "
249   - f"{spu['image_height']}, '{spu['image_path']}', '{spu['image_alt']}', "
250   - f"'{spu['inventory_policy']}', {spu['inventory_quantity']}, "
251   - f"'{spu['inventory_tracking']}', {spu['published']}, "
252   - f"'{spu['published_at']}', {spu['requires_shipping']}, {spu['taxable']}, "
  259 + f"({spu['id']}, {spu['shop_id']}, '{escape_sql_string(spu['shoplazza_id'])}', "
  260 + f"'{escape_sql_string(spu['handle'])}', '{escape_sql_string(spu['title'])}', "
  261 + f"'{escape_sql_string(spu['brief'])}', '{escape_sql_string(spu['description'])}', "
  262 + f"'{escape_sql_string(spu['spu'])}', '{escape_sql_string(spu['vendor'])}', "
  263 + f"'{escape_sql_string(spu['vendor_url'])}', '{escape_sql_string(spu['seo_title'])}', "
  264 + f"'{escape_sql_string(spu['seo_description'])}', '{escape_sql_string(spu['seo_keywords'])}', "
  265 + f"'{escape_sql_string(spu['image_src'])}', {spu['image_width']}, "
  266 + f"{spu['image_height']}, '{escape_sql_string(spu['image_path'])}', "
  267 + f"'{escape_sql_string(spu['image_alt'])}', '{escape_sql_string(spu['inventory_policy'])}', "
  268 + f"{spu['inventory_quantity']}, '{escape_sql_string(spu['inventory_tracking'])}', "
  269 + f"{spu['published']}, '{escape_sql_string(spu['published_at'])}', "
  270 + f"{spu['requires_shipping']}, {spu['taxable']}, "
253 271 f"{spu['fake_sales']}, {spu['display_fake_sales']}, {spu['mixed_wholesale']}, "
254 272 f"{spu['need_variant_image']}, {spu['has_only_default_variant']}, "
255   - f"'{spu['tags']}', '{spu['note']}', '{spu['category']}', "
256   - f"'{spu['shoplazza_created_at']}', '{spu['shoplazza_updated_at']}', "
257   - f"'{spu['tenant_id']}', '{spu['creator']}', '{spu['create_time']}', "
258   - f"'{spu['updater']}', '{spu['update_time']}', {spu['deleted']})"
  273 + f"'{escape_sql_string(spu['tags'])}', '{escape_sql_string(spu['note'])}', "
  274 + f"'{escape_sql_string(spu['category'])}', '{escape_sql_string(spu['shoplazza_created_at'])}', "
  275 + f"'{escape_sql_string(spu['shoplazza_updated_at'])}', '{escape_sql_string(spu['tenant_id'])}', "
  276 + f"'{escape_sql_string(spu['creator'])}', '{escape_sql_string(spu['create_time'])}', "
  277 + f"'{escape_sql_string(spu['updater'])}', '{escape_sql_string(spu['update_time'])}', "
  278 + f"{spu['deleted']})"
259 279 )
260 280 f.write(values)
261 281 if i < len(spus) - 1:
... ... @@ -274,18 +294,24 @@ def generate_sql_inserts(spus: list, skus: list, output_file: str):
274 294 f.write(") VALUES\n")
275 295  
276 296 for i, sku in enumerate(skus):
  297 + # Handle extend field (JSON, can be NULL)
  298 + extend_value = 'NULL' if sku['extend'] is None else f"'{escape_sql_string(sku['extend'])}'"
  299 +
277 300 values = (
278   - f"({sku['id']}, {sku['spu_id']}, {sku['shop_id']}, '{sku['shoplazza_id']}', "
279   - f"'{sku['shoplazza_product_id']}', '{sku['shoplazza_image_id']}', "
280   - f"'{sku['title']}', '{sku['sku']}', '{sku['barcode']}', {sku['position']}, "
  301 + f"({sku['id']}, {sku['spu_id']}, {sku['shop_id']}, '{escape_sql_string(sku['shoplazza_id'])}', "
  302 + f"'{escape_sql_string(sku['shoplazza_product_id'])}', '{escape_sql_string(sku['shoplazza_image_id'])}', "
  303 + f"'{escape_sql_string(sku['title'])}', '{escape_sql_string(sku['sku'])}', "
  304 + f"'{escape_sql_string(sku['barcode'])}', {sku['position']}, "
281 305 f"{sku['price']}, {sku['compare_at_price']}, {sku['cost_price']}, "
282   - f"'{sku['option1']}', '{sku['option2']}', '{sku['option3']}', "
283   - f"{sku['inventory_quantity']}, {sku['weight']}, '{sku['weight_unit']}', "
284   - f"'{sku['image_src']}', '{sku['wholesale_price']}', '{sku['note']}', "
285   - f"'{sku['extend']}', '{sku['shoplazza_created_at']}', "
286   - f"'{sku['shoplazza_updated_at']}', '{sku['tenant_id']}', "
287   - f"'{sku['creator']}', '{sku['create_time']}', '{sku['updater']}', "
288   - f"'{sku['update_time']}', {sku['deleted']})"
  306 + f"'{escape_sql_string(sku['option1'])}', '{escape_sql_string(sku['option2'])}', "
  307 + f"'{escape_sql_string(sku['option3'])}', {sku['inventory_quantity']}, {sku['weight']}, "
  308 + f"'{escape_sql_string(sku['weight_unit'])}', '{escape_sql_string(sku['image_src'])}', "
  309 + f"'{escape_sql_string(sku['wholesale_price'])}', '{escape_sql_string(sku['note'])}', "
  310 + f"{extend_value}, '{escape_sql_string(sku['shoplazza_created_at'])}', "
  311 + f"'{escape_sql_string(sku['shoplazza_updated_at'])}', '{escape_sql_string(sku['tenant_id'])}', "
  312 + f"'{escape_sql_string(sku['creator'])}', '{escape_sql_string(sku['create_time'])}', "
  313 + f"'{escape_sql_string(sku['updater'])}', '{escape_sql_string(sku['update_time'])}', "
  314 + f"{sku['deleted']})"
289 315 )
290 316 f.write(values)
291 317 if i < len(skus) - 1:
... ...
scripts/import_test_data.py
... ... @@ -24,11 +24,25 @@ def import_sql_file(db_engine, sql_file: str):
24 24 db_engine: SQLAlchemy database engine
25 25 sql_file: Path to SQL file
26 26 """
  27 + from sqlalchemy import text
  28 +
27 29 with open(sql_file, 'r', encoding='utf-8') as f:
28 30 sql_content = f.read()
29 31  
30 32 # Split by semicolons to get individual statements
31   - statements = [s.strip() for s in sql_content.split(';') if s.strip() and not s.strip().startswith('--')]
  33 + # Remove comments and empty lines, then split by semicolon
  34 + lines = []
  35 + for line in sql_content.split('\n'):
  36 + # Remove inline comments
  37 + if '--' in line:
  38 + line = line[:line.index('--')]
  39 + line = line.strip()
  40 + if line and not line.startswith('--'):
  41 + lines.append(line)
  42 +
  43 + # Join lines and split by semicolon
  44 + full_text = ' '.join(lines)
  45 + statements = [s.strip() for s in full_text.split(';') if s.strip()]
32 46  
33 47 print(f"Executing {len(statements)} SQL statements...")
34 48  
... ... @@ -36,12 +50,12 @@ def import_sql_file(db_engine, sql_file: str):
36 50 for i, statement in enumerate(statements, 1):
37 51 if statement:
38 52 try:
39   - conn.execute(statement)
  53 + conn.execute(text(statement))
40 54 conn.commit()
41 55 print(f" [{i}/{len(statements)}] Executed successfully")
42 56 except Exception as e:
43 57 print(f" [{i}/{len(statements)}] ERROR: {e}")
44   - print(f" Statement: {statement[:100]}...")
  58 + print(f" Statement: {statement[:200]}...")
45 59 raise
46 60  
47 61  
... ... @@ -109,6 +123,22 @@ def main():
109 123  
110 124 print("Database connection successful")
111 125  
  126 + # Clean existing data if tenant_id provided
  127 + if args.tenant_id:
  128 + print(f"\nCleaning existing data for tenant_id: {args.tenant_id}")
  129 + from sqlalchemy import text
  130 + try:
  131 + with db_engine.connect() as conn:
  132 + # Delete SKUs first (foreign key constraint)
  133 + conn.execute(text(f"DELETE FROM shoplazza_product_sku WHERE tenant_id = '{args.tenant_id}'"))
  134 + # Delete SPUs
  135 + conn.execute(text(f"DELETE FROM shoplazza_product_spu WHERE tenant_id = '{args.tenant_id}'"))
  136 + conn.commit()
  137 + print("✓ Existing data cleaned")
  138 + except Exception as e:
  139 + print(f"⚠ Warning: Failed to clean existing data: {e}")
  140 + # Continue anyway
  141 +
112 142 # Import SQL file
113 143 print(f"\nImporting SQL file: {args.sql_file}")
114 144 try:
... ...
scripts/ingest_shoplazza.py
... ... @@ -78,11 +78,24 @@ def main():
78 78 print(f"ERROR: Failed to connect to MySQL: {e}")
79 79 return 1
80 80  
81   - # Connect to Elasticsearch
82   - print(f"Connecting to Elasticsearch: {args.es_host}")
83   - es_client = ESClient(hosts=[args.es_host])
  81 + # Connect to Elasticsearch (use unified config loading)
  82 + from config.env_config import get_es_config
  83 + es_config = get_es_config()
  84 +
  85 + # Use provided es_host or fallback to config
  86 + es_host = args.es_host or es_config.get('host', 'http://localhost:9200')
  87 + es_username = es_config.get('username')
  88 + es_password = es_config.get('password')
  89 +
  90 + print(f"Connecting to Elasticsearch: {es_host}")
  91 + if es_username and es_password:
  92 + print(f"Using authentication: {es_username}")
  93 + es_client = ESClient(hosts=[es_host], username=es_username, password=es_password)
  94 + else:
  95 + es_client = ESClient(hosts=[es_host])
  96 +
84 97 if not es_client.ping():
85   - print(f"ERROR: Cannot connect to Elasticsearch at {args.es_host}")
  98 + print(f"ERROR: Cannot connect to Elasticsearch at {es_host}")
86 99 return 1
87 100  
88 101 # Generate and create index
... ...
test_data_base.sql 0 → 100644
... ... @@ -0,0 +1,69 @@
  1 +-- SPU Test Data
  2 +INSERT INTO shoplazza_product_spu (
  3 + id, shop_id, shoplazza_id, handle, title, brief, description, spu,
  4 + vendor, vendor_url, seo_title, seo_description, seo_keywords,
  5 + image_src, image_width, image_height, image_path, image_alt,
  6 + inventory_policy, inventory_quantity, inventory_tracking,
  7 + published, published_at, requires_shipping, taxable,
  8 + fake_sales, display_fake_sales, mixed_wholesale, need_variant_image,
  9 + has_only_default_variant, tags, note, category,
  10 + shoplazza_created_at, shoplazza_updated_at, tenant_id,
  11 + creator, create_time, updater, update_time, deleted
  12 +) VALUES
  13 +(1, 1, 'spu-1', 'product-1', '音响 海尔', '蓝牙无线音响', '<p>蓝牙无线音响,来自海尔品牌。Bluetooth wireless speaker</p>', '', '海尔', 'https://海尔.com', '音响 海尔 - 服装', '购买海尔音响,蓝牙无线音响', '音响,海尔,服装', '//cdn.example.com/products/1.jpg', 800, 600, 'products/1.jpg', '音响 海尔', '', 0, '0', 1, '2025-10-29 12:17:09', 1, 0, 0, 0, 0, 0, 0, '服装,海尔,音响', '', '服装', '2025-10-29 12:17:09', '2025-11-05 12:17:09', '1', '1', '2025-10-29 12:17:09', '1', '2025-11-05 12:17:09', 0),
  14 +(2, 1, 'spu-2', 'product-2', '无线鼠标 华为', '人体工学无线鼠标', '<p>人体工学无线鼠标,来自华为品牌。Ergonomic wireless mouse</p>', '', '华为', 'https://华为.com', '无线鼠标 华为 - 服装', '购买华为无线鼠标,人体工学无线鼠标', '无线鼠标,华为,服装', '//cdn.example.com/products/2.jpg', 800, 600, 'products/2.jpg', '无线鼠标 华为', '', 0, '0', 1, '2025-09-30 12:17:09', 1, 0, 0, 0, 0, 0, 0, '服装,华为,无线鼠标', '', '服装', '2025-09-30 12:17:09', '2025-10-01 12:17:09', '1', '1', '2025-09-30 12:17:09', '1', '2025-10-01 12:17:09', 0),
  15 +(3, 1, 'spu-3', 'product-3', '显示器 Sony', '4K高清显示器', '<p>4K高清显示器,来自Sony品牌。4K high-definition monitor</p>', '', 'Sony', 'https://sony.com', '显示器 Sony - 服装', '购买Sony显示器,4K高清显示器', '显示器,Sony,服装', '//cdn.example.com/products/3.jpg', 800, 600, 'products/3.jpg', '显示器 Sony', '', 0, '0', 1, '2025-03-30 12:17:09', 1, 0, 0, 0, 0, 0, 0, '服装,Sony,显示器', '', '服装', '2025-03-30 12:17:09', '2025-04-05 12:17:09', '1', '1', '2025-03-30 12:17:09', '1', '2025-04-05 12:17:09', 0),
  16 +(4, 1, 'spu-4', 'product-4', '蓝牙耳机 Apple', '高品质无线蓝牙耳机', '<p>高品质无线蓝牙耳机,来自Apple品牌。High-quality wireless Bluetooth headphone</p>', '', 'Apple', 'https://apple.com', '蓝牙耳机 Apple - 电子产品', '购买Apple蓝牙耳机,高品质无线蓝牙耳机', '蓝牙耳机,Apple,电子产品', '//cdn.example.com/products/4.jpg', 800, 600, 'products/4.jpg', '蓝牙耳机 Apple', '', 0, '0', 1, '2025-05-28 12:17:09', 1, 0, 0, 0, 0, 0, 0, '电子产品,Apple,蓝牙耳机', '', '电子产品', '2025-05-28 12:17:09', '2025-06-08 12:17:09', '1', '1', '2025-05-28 12:17:09', '1', '2025-06-08 12:17:09', 0),
  17 +(5, 1, 'spu-5', 'product-5', '智能手表 海尔', '多功能智能手表', '<p>多功能智能手表,来自海尔品牌。Multi-function smart watch</p>', '', '海尔', 'https://海尔.com', '智能手表 海尔 - 图书', '购买海尔智能手表,多功能智能手表', '智能手表,海尔,图书', '//cdn.example.com/products/5.jpg', 800, 600, 'products/5.jpg', '智能手表 海尔', '', 0, '0', 1, '2025-08-13 12:17:09', 1, 0, 0, 0, 0, 0, 0, '图书,海尔,智能手表', '', '图书', '2025-08-13 12:17:09', '2025-08-26 12:17:09', '1', '1', '2025-08-13 12:17:09', '1', '2025-08-26 12:17:09', 0),
  18 +(6, 1, 'spu-6', 'product-6', '无线鼠标 Samsung', '人体工学无线鼠标', '<p>人体工学无线鼠标,来自Samsung品牌。Ergonomic wireless mouse</p>', '', 'Samsung', 'https://samsung.com', '无线鼠标 Samsung - 服装', '购买Samsung无线鼠标,人体工学无线鼠标', '无线鼠标,Samsung,服装', '//cdn.example.com/products/6.jpg', 800, 600, 'products/6.jpg', '无线鼠标 Samsung', '', 0, '0', 1, '2025-05-28 12:17:09', 1, 0, 0, 0, 0, 0, 0, '服装,Samsung,无线鼠标', '', '服装', '2025-05-28 12:17:09', '2025-06-03 12:17:09', '1', '1', '2025-05-28 12:17:09', '1', '2025-06-03 12:17:09', 0),
  19 +(7, 1, 'spu-7', 'product-7', '显示器 美的', '4K高清显示器', '<p>4K高清显示器,来自美的品牌。4K high-definition monitor</p>', '', '美的', 'https://美的.com', '显示器 美的 - 电子产品', '购买美的显示器,4K高清显示器', '显示器,美的,电子产品', '//cdn.example.com/products/7.jpg', 800, 600, 'products/7.jpg', '显示器 美的', '', 0, '0', 1, '2025-02-03 12:17:09', 1, 0, 0, 0, 0, 0, 0, '电子产品,美的,显示器', '', '电子产品', '2025-02-03 12:17:09', '2025-02-28 12:17:09', '1', '1', '2025-02-03 12:17:09', '1', '2025-02-28 12:17:09', 0),
  20 +(8, 1, 'spu-8', 'product-8', '智能手机 华为', '高性能智能手机', '<p>高性能智能手机,来自华为品牌。High-performance smartphone</p>', '', '华为', 'https://华为.com', '智能手机 华为 - 运动用品', '购买华为智能手机,高性能智能手机', '智能手机,华为,运动用品', '//cdn.example.com/products/8.jpg', 800, 600, 'products/8.jpg', '智能手机 华为', '', 0, '0', 1, '2025-08-21 12:17:09', 1, 0, 0, 0, 0, 0, 0, '运动用品,华为,智能手机', '', '运动用品', '2025-08-21 12:17:09', '2025-09-02 12:17:09', '1', '1', '2025-08-21 12:17:09', '1', '2025-09-02 12:17:09', 0),
  21 +(9, 1, 'spu-9', 'product-9', '运动鞋 Apple', '舒适透气的运动鞋', '<p>舒适透气的运动鞋,来自Apple品牌。Comfortable and breathable running shoes</p>', '', 'Apple', 'https://apple.com', '运动鞋 Apple - 图书', '购买Apple运动鞋,舒适透气的运动鞋', '运动鞋,Apple,图书', '//cdn.example.com/products/9.jpg', 800, 600, 'products/9.jpg', '运动鞋 Apple', '', 0, '0', 1, '2025-07-22 12:17:09', 1, 0, 0, 0, 0, 0, 0, '图书,Apple,运动鞋', '', '图书', '2025-07-22 12:17:09', '2025-08-03 12:17:09', '1', '1', '2025-07-22 12:17:09', '1', '2025-08-03 12:17:09', 0),
  22 +(10, 1, 'spu-10', 'product-10', '机械键盘 Sony', 'RGB背光机械键盘', '<p>RGB背光机械键盘,来自Sony品牌。RGB backlit mechanical keyboard</p>', '', 'Sony', 'https://sony.com', '机械键盘 Sony - 电子产品', '购买Sony机械键盘,RGB背光机械键盘', '机械键盘,Sony,电子产品', '//cdn.example.com/products/10.jpg', 800, 600, 'products/10.jpg', '机械键盘 Sony', '', 0, '0', 1, '2025-02-24 12:17:09', 1, 0, 0, 0, 0, 0, 0, '电子产品,Sony,机械键盘', '', '电子产品', '2025-02-24 12:17:09', '2025-03-25 12:17:09', '1', '1', '2025-02-24 12:17:09', '1', '2025-03-25 12:17:09', 0);
  23 +
  24 +-- SKU Test Data
  25 +INSERT INTO shoplazza_product_sku (
  26 + id, spu_id, shop_id, shoplazza_id, shoplazza_product_id, shoplazza_image_id,
  27 + title, sku, barcode, position, price, compare_at_price, cost_price,
  28 + option1, option2, option3, inventory_quantity, weight, weight_unit,
  29 + image_src, wholesale_price, note, extend,
  30 + shoplazza_created_at, shoplazza_updated_at, tenant_id,
  31 + creator, create_time, updater, update_time, deleted
  32 +) VALUES
  33 +(1, 1, 1, 'sku-1', 'spu-1', '', '灰色 / S', 'SKU-1-1', 'BAR00000001', 1, 256.65, 315.84, 153.99, '灰色', 'S', '', 83, 2.19, 'kg', '', '[{"price": 205.32, "minQuantity": 10}]', '', NULL, '2025-02-01 12:17:09', '2025-02-26 12:17:09', '1', '1', '2025-02-01 12:17:09', '1', '2025-02-26 12:17:09', 0),
  34 +(2, 1, 1, 'sku-2', 'spu-1', '', '绿色 / XXL', 'SKU-1-2', 'BAR00000002', 2, 274.8, 345.42, 164.88, '绿色', 'XXL', '', 81, 2.73, 'kg', '', '[{"price": 219.84, "minQuantity": 10}]', '', NULL, '2025-09-04 12:17:09', '2025-09-18 12:17:09', '1', '1', '2025-09-04 12:17:09', '1', '2025-09-18 12:17:09', 0),
  35 +(3, 1, 1, 'sku-3', 'spu-1', '', '黑色 / XL', 'SKU-1-3', 'BAR00000003', 3, 245.37, 320.02, 147.22, '黑色', 'XL', '', 53, 0.28, 'kg', '', '[{"price": 196.3, "minQuantity": 10}]', '', NULL, '2025-07-20 12:17:09', '2025-07-23 12:17:09', '1', '1', '2025-07-20 12:17:09', '1', '2025-07-23 12:17:09', 0),
  36 +(4, 1, 1, 'sku-4', 'spu-1', '', '红色 / M', 'SKU-1-4', 'BAR00000004', 4, 238.24, 332.91, 142.94, '红色', 'M', '', 71, 3.23, 'kg', '', '[{"price": 190.59, "minQuantity": 10}]', '', NULL, '2025-06-30 12:17:09', '2025-07-01 12:17:09', '1', '1', '2025-06-30 12:17:09', '1', '2025-07-01 12:17:09', 0),
  37 +(5, 2, 1, 'sku-5', 'spu-2', '', '黑色 / L', 'SKU-2-1', 'BAR00000005', 1, 449.1, 659.64, 269.46, '黑色', 'L', '', 88, 1.54, 'kg', '', '[{"price": 359.28, "minQuantity": 10}]', '', NULL, '2025-07-30 12:17:09', '2025-08-04 12:17:09', '1', '1', '2025-07-30 12:17:09', '1', '2025-08-04 12:17:09', 0),
  38 +(6, 2, 1, 'sku-6', 'spu-2', '', '绿色 / M', 'SKU-2-2', 'BAR00000006', 2, 385.8, 510.27, 231.48, '绿色', 'M', '', 90, 2.78, 'kg', '', '[{"price": 308.64, "minQuantity": 10}]', '', NULL, '2024-12-21 12:17:09', '2024-12-23 12:17:09', '1', '1', '2024-12-21 12:17:09', '1', '2024-12-23 12:17:09', 0),
  39 +(7, 2, 1, 'sku-7', 'spu-2', '', '白色 / XXL', 'SKU-2-3', 'BAR00000007', 3, 444.82, 652.28, 266.89, '白色', 'XXL', '', 4, 1.1, 'kg', '', '[{"price": 355.86, "minQuantity": 10}]', '', NULL, '2025-07-23 12:17:09', '2025-07-25 12:17:09', '1', '1', '2025-07-23 12:17:09', '1', '2025-07-25 12:17:09', 0),
  40 +(8, 2, 1, 'sku-8', 'spu-2', '', '蓝色 / M', 'SKU-2-4', 'BAR00000008', 4, 412.17, 574.41, 247.3, '蓝色', 'M', '', 90, 4.34, 'kg', '', '[{"price": 329.73, "minQuantity": 10}]', '', NULL, '2025-04-01 12:17:09', '2025-04-15 12:17:09', '1', '1', '2025-04-01 12:17:09', '1', '2025-04-15 12:17:09', 0),
  41 +(9, 3, 1, 'sku-9', 'spu-3', '', '白色 / S', 'SKU-3-1', 'BAR00000009', 1, 424.04, 542.91, 254.42, '白色', 'S', '', 75, 1.6, 'kg', '', '[{"price": 339.23, "minQuantity": 10}]', '', NULL, '2025-07-08 12:17:09', '2025-07-24 12:17:09', '1', '1', '2025-07-08 12:17:09', '1', '2025-07-24 12:17:09', 0),
  42 +(10, 3, 1, 'sku-10', 'spu-3', '', '蓝色 / XXL', 'SKU-3-2', 'BAR00000010', 2, 446.94, 555.31, 268.16, '蓝色', 'XXL', '', 36, 4.5, 'kg', '', '[{"price": 357.55, "minQuantity": 10}]', '', NULL, '2025-06-20 12:17:09', '2025-06-22 12:17:09', '1', '1', '2025-06-20 12:17:09', '1', '2025-06-22 12:17:09', 0),
  43 +(11, 3, 1, 'sku-11', 'spu-3', '', '灰色 / S', 'SKU-3-3', 'BAR00000011', 3, 423.72, 606.22, 254.23, '灰色', 'S', '', 77, 3.42, 'kg', '', '[{"price": 338.97, "minQuantity": 10}]', '', NULL, '2025-09-17 12:17:09', '2025-09-21 12:17:09', '1', '1', '2025-09-17 12:17:09', '1', '2025-09-21 12:17:09', 0),
  44 +(12, 3, 1, 'sku-12', 'spu-3', '', '灰色 / S', 'SKU-3-4', 'BAR00000012', 4, 416.76, 525.39, 250.06, '灰色', 'S', '', 79, 4.83, 'kg', '', '[{"price": 333.41, "minQuantity": 10}]', '', NULL, '2025-07-26 12:17:09', '2025-08-10 12:17:09', '1', '1', '2025-07-26 12:17:09', '1', '2025-08-10 12:17:09', 0),
  45 +(13, 4, 1, 'sku-13', 'spu-4', '', '灰色 / M', 'SKU-4-1', 'BAR00000013', 1, 452.46, 549.77, 271.48, '灰色', 'M', '', 16, 1.68, 'kg', '', '[{"price": 361.97, "minQuantity": 10}]', '', NULL, '2025-10-26 12:17:09', '2025-11-05 12:17:09', '1', '1', '2025-10-26 12:17:09', '1', '2025-11-05 12:17:09', 0),
  46 +(14, 4, 1, 'sku-14', 'spu-4', '', '绿色 / L', 'SKU-4-2', 'BAR00000014', 2, 425.48, 514.03, 255.29, '绿色', 'L', '', 24, 3.86, 'kg', '', '[{"price": 340.38, "minQuantity": 10}]', '', NULL, '2025-07-10 12:17:09', '2025-07-27 12:17:09', '1', '1', '2025-07-10 12:17:09', '1', '2025-07-27 12:17:09', 0),
  47 +(15, 4, 1, 'sku-15', 'spu-4', '', '黑色 / S', 'SKU-4-3', 'BAR00000015', 3, 454.51, 652.31, 272.71, '黑色', 'S', '', 50, 0.15, 'kg', '', '[{"price": 363.61, "minQuantity": 10}]', '', NULL, '2025-05-15 12:17:09', '2025-05-21 12:17:09', '1', '1', '2025-05-15 12:17:09', '1', '2025-05-21 12:17:09', 0),
  48 +(16, 4, 1, 'sku-16', 'spu-4', '', '蓝色 / S', 'SKU-4-4', 'BAR00000016', 4, 428.14, 613.36, 256.88, '蓝色', 'S', '', 36, 4.19, 'kg', '', '[{"price": 342.51, "minQuantity": 10}]', '', NULL, '2025-09-24 12:17:09', '2025-10-15 12:17:09', '1', '1', '2025-09-24 12:17:09', '1', '2025-10-15 12:17:09', 0),
  49 +(17, 5, 1, 'sku-17', 'spu-5', '', '白色 / L', 'SKU-5-1', 'BAR00000017', 1, 250.44, 304.72, 150.26, '白色', 'L', '', 82, 3.73, 'kg', '', '[{"price": 200.35, "minQuantity": 10}]', '', NULL, '2025-05-19 12:17:09', '2025-05-29 12:17:09', '1', '1', '2025-05-19 12:17:09', '1', '2025-05-29 12:17:09', 0),
  50 +(18, 5, 1, 'sku-18', 'spu-5', '', '绿色 / S', 'SKU-5-2', 'BAR00000018', 2, 276.79, 404.72, 166.07, '绿色', 'S', '', 81, 2.9, 'kg', '', '[{"price": 221.43, "minQuantity": 10}]', '', NULL, '2025-05-05 12:17:09', '2025-05-16 12:17:09', '1', '1', '2025-05-05 12:17:09', '1', '2025-05-16 12:17:09', 0),
  51 +(19, 5, 1, 'sku-19', 'spu-5', '', '蓝色 / XXL', 'SKU-5-3', 'BAR00000019', 3, 238.73, 336.81, 143.24, '蓝色', 'XXL', '', 8, 0.62, 'kg', '', '[{"price": 190.99, "minQuantity": 10}]', '', NULL, '2025-06-21 12:17:09', '2025-06-29 12:17:09', '1', '1', '2025-06-21 12:17:09', '1', '2025-06-29 12:17:09', 0),
  52 +(20, 5, 1, 'sku-20', 'spu-5', '', '蓝色 / L', 'SKU-5-4', 'BAR00000020', 4, 279.88, 413.66, 167.93, '蓝色', 'L', '', 42, 2.78, 'kg', '', '[{"price": 223.9, "minQuantity": 10}]', '', NULL, '2025-09-11 12:17:09', '2025-10-09 12:17:09', '1', '1', '2025-09-11 12:17:09', '1', '2025-10-09 12:17:09', 0),
  53 +(21, 6, 1, 'sku-21', 'spu-6', '', '蓝色 / L', 'SKU-6-1', 'BAR00000021', 1, 527.6, 710.02, 316.56, '蓝色', 'L', '', 32, 4.36, 'kg', '', '[{"price": 422.08, "minQuantity": 10}]', '', NULL, '2024-11-29 12:17:09', '2024-12-18 12:17:09', '1', '1', '2024-11-29 12:17:09', '1', '2024-12-18 12:17:09', 0),
  54 +(22, 6, 1, 'sku-22', 'spu-6', '', '白色 / L', 'SKU-6-2', 'BAR00000022', 2, 516.8, 770.98, 310.08, '白色', 'L', '', 69, 1.83, 'kg', '', '[{"price": 413.44, "minQuantity": 10}]', '', NULL, '2025-05-22 12:17:09', '2025-06-12 12:17:09', '1', '1', '2025-05-22 12:17:09', '1', '2025-06-12 12:17:09', 0),
  55 +(23, 6, 1, 'sku-23', 'spu-6', '', '红色 / S', 'SKU-6-3', 'BAR00000023', 3, 485.59, 598.04, 291.36, '红色', 'S', '', 79, 3.85, 'kg', '', '[{"price": 388.47, "minQuantity": 10}]', '', NULL, '2024-11-24 12:17:09', '2024-12-17 12:17:09', '1', '1', '2024-11-24 12:17:09', '1', '2024-12-17 12:17:09', 0),
  56 +(24, 7, 1, 'sku-24', 'spu-7', '', '蓝色 / XXL', 'SKU-7-1', 'BAR00000024', 1, 161.95, 231.09, 97.17, '蓝色', 'XXL', '', 49, 4.62, 'kg', '', '[{"price": 129.56, "minQuantity": 10}]', '', NULL, '2025-04-20 12:17:09', '2025-04-24 12:17:09', '1', '1', '2025-04-20 12:17:09', '1', '2025-04-24 12:17:09', 0),
  57 +(25, 7, 1, 'sku-25', 'spu-7', '', '黑色 / S', 'SKU-7-2', 'BAR00000025', 2, 148.66, 211.66, 89.2, '黑色', 'S', '', 20, 1.5, 'kg', '', '[{"price": 118.93, "minQuantity": 10}]', '', NULL, '2025-04-28 12:17:09', '2025-05-16 12:17:09', '1', '1', '2025-04-28 12:17:09', '1', '2025-05-16 12:17:09', 0),
  58 +(26, 7, 1, 'sku-26', 'spu-7', '', '黑色 / XXL', 'SKU-7-3', 'BAR00000026', 3, 173.53, 213.88, 104.12, '黑色', 'XXL', '', 2, 4.43, 'kg', '', '[{"price": 138.82, "minQuantity": 10}]', '', NULL, '2024-12-16 12:17:09', '2024-12-17 12:17:09', '1', '1', '2024-12-16 12:17:09', '1', '2024-12-17 12:17:09', 0),
  59 +(27, 7, 1, 'sku-27', 'spu-7', '', '黑色 / S', 'SKU-7-4', 'BAR00000027', 4, 177.7, 233.07, 106.62, '黑色', 'S', '', 73, 2.65, 'kg', '', '[{"price": 142.16, "minQuantity": 10}]', '', NULL, '2025-08-29 12:17:09', '2025-09-23 12:17:09', '1', '1', '2025-08-29 12:17:09', '1', '2025-09-23 12:17:09', 0),
  60 +(28, 8, 1, 'sku-28', 'spu-8', '', '白色 / XL', 'SKU-8-1', 'BAR00000028', 1, 471.42, 690.0, 282.85, '白色', 'XL', '', 72, 1.76, 'kg', '', '[{"price": 377.13, "minQuantity": 10}]', '', NULL, '2025-01-31 12:17:09', '2025-02-17 12:17:09', '1', '1', '2025-01-31 12:17:09', '1', '2025-02-17 12:17:09', 0),
  61 +(29, 8, 1, 'sku-29', 'spu-8', '', '黑色 / S', 'SKU-8-2', 'BAR00000029', 2, 445.7, 585.74, 267.42, '黑色', 'S', '', 62, 0.59, 'kg', '', '[{"price": 356.56, "minQuantity": 10}]', '', NULL, '2025-04-05 12:17:09', '2025-04-25 12:17:09', '1', '1', '2025-04-05 12:17:09', '1', '2025-04-25 12:17:09', 0),
  62 +(30, 8, 1, 'sku-30', 'spu-8', '', '灰色 / L', 'SKU-8-3', 'BAR00000030', 3, 477.89, 605.71, 286.74, '灰色', 'L', '', 1, 2.19, 'kg', '', '[{"price": 382.31, "minQuantity": 10}]', '', NULL, '2025-09-19 12:17:09', '2025-10-06 12:17:09', '1', '1', '2025-09-19 12:17:09', '1', '2025-10-06 12:17:09', 0),
  63 +(31, 9, 1, 'sku-31', 'spu-9', '', '红色 / XL', 'SKU-9-1', 'BAR00000031', 1, 432.85, 526.5, 259.71, '红色', 'XL', '', 44, 1.11, 'kg', '', '[{"price": 346.28, "minQuantity": 10}]', '', NULL, '2024-12-13 12:17:09', '2024-12-15 12:17:09', '1', '1', '2024-12-13 12:17:09', '1', '2024-12-15 12:17:09', 0),
  64 +(32, 9, 1, 'sku-32', 'spu-9', '', '红色 / XXL', 'SKU-9-2', 'BAR00000032', 2, 448.02, 597.6, 268.81, '红色', 'XXL', '', 18, 4.56, 'kg', '', '[{"price": 358.42, "minQuantity": 10}]', '', NULL, '2025-08-19 12:17:09', '2025-09-17 12:17:09', '1', '1', '2025-08-19 12:17:09', '1', '2025-09-17 12:17:09', 0),
  65 +(33, 9, 1, 'sku-33', 'spu-9', '', '黑色 / XXL', 'SKU-9-3', 'BAR00000033', 3, 423.8, 631.05, 254.28, '黑色', 'XXL', '', 21, 5.0, 'kg', '', '[{"price": 339.04, "minQuantity": 10}]', '', NULL, '2025-05-29 12:17:09', '2025-06-05 12:17:09', '1', '1', '2025-05-29 12:17:09', '1', '2025-06-05 12:17:09', 0),
  66 +(34, 9, 1, 'sku-34', 'spu-9', '', '灰色 / XXL', 'SKU-9-4', 'BAR00000034', 4, 424.56, 557.45, 254.73, '灰色', 'XXL', '', 70, 0.17, 'kg', '', '[{"price": 339.64, "minQuantity": 10}]', '', NULL, '2025-05-30 12:17:09', '2025-06-11 12:17:09', '1', '1', '2025-05-30 12:17:09', '1', '2025-06-11 12:17:09', 0),
  67 +(35, 9, 1, 'sku-35', 'spu-9', '', '绿色 / XXL', 'SKU-9-5', 'BAR00000035', 5, 441.55, 568.31, 264.93, '绿色', 'XXL', '', 44, 1.73, 'kg', '', '[{"price": 353.24, "minQuantity": 10}]', '', NULL, '2025-03-09 12:17:09', '2025-03-15 12:17:09', '1', '1', '2025-03-09 12:17:09', '1', '2025-03-15 12:17:09', 0),
  68 +(36, 10, 1, 'sku-36', 'spu-10', '', '绿色', 'SKU-10-1', 'BAR00000036', 1, 99.88, 120.43, 59.93, '绿色', '', '', 98, 1.93, 'kg', '', '[{"price": 79.9, "minQuantity": 10}]', '', NULL, '2024-12-25 12:17:09', '2025-01-09 12:17:09', '1', '1', '2024-12-25 12:17:09', '1', '2025-01-09 12:17:09', 0),
  69 +(37, 10, 1, 'sku-37', 'spu-10', '', '蓝色', 'SKU-10-2', 'BAR00000037', 2, 110.96, 140.29, 66.58, '蓝色', '', '', 100, 1.37, 'kg', '', '[{"price": 88.77, "minQuantity": 10}]', '', NULL, '2025-05-10 12:17:09', '2025-05-26 12:17:09', '1', '1', '2025-05-10 12:17:09', '1', '2025-05-26 12:17:09', 0);
... ...
utils/db_connector.py
... ... @@ -2,7 +2,7 @@
2 2 Database connector utility for MySQL connections.
3 3 """
4 4  
5   -from sqlalchemy import create_engine
  5 +from sqlalchemy import create_engine, text
6 6 from sqlalchemy.pool import QueuePool
7 7 from typing import Dict, Any, Optional
8 8 import pymysql
... ... @@ -84,7 +84,8 @@ def test_connection(engine) -&gt; bool:
84 84 """
85 85 try:
86 86 with engine.connect() as conn:
87   - conn.execute("SELECT 1")
  87 + conn.execute(text("SELECT 1"))
  88 + conn.commit()
88 89 return True
89 90 except Exception as e:
90 91 print(f"Database connection test failed: {e}")
... ...