diff --git a/.gitignore b/.gitignore index ae2c06c..d865f33 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ frontend-vue/node_modules/ # frontend-vue build output frontend-vue/dist/ +frontend-vue/new_web_source # generated type declarations frontend-vue/auto-imports.d.ts @@ -28,3 +29,6 @@ backend-java/src/main/resources/application-local.yml # backend-java IDE files backend-java/.idea/ backend-java/*.iml + +# desktop app +desktop/ diff --git a/1.xlsx b/1.xlsx new file mode 100644 index 0000000..a6e124d Binary files /dev/null and b/1.xlsx differ diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..0b38d5f --- /dev/null +++ b/app/.env @@ -0,0 +1,12 @@ +base_url=http://159.75.121.33:15124 +workflow_id=7608812635877900322 +mysql_host=159.75.121.33 +mysql_user=AIimage + +proxy_url=https://api.jikip.com/ip-get?num=1&minute=1&format=json&area=all&protocol=1&mode=2&key=r4m8dov52giup2o +proxy_mode=2 + +client_name=NanriAI +java_api_base=http://127.0.0.1:18080 + + diff --git a/app/__pycache__/app.cpython-312.pyc b/app/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000..234a9f4 Binary files /dev/null and b/app/__pycache__/app.cpython-312.pyc differ diff --git a/app/__pycache__/app_common.cpython-312.pyc b/app/__pycache__/app_common.cpython-312.pyc new file mode 100644 index 0000000..92c7675 Binary files /dev/null and b/app/__pycache__/app_common.cpython-312.pyc differ diff --git a/app/__pycache__/config.cpython-312.pyc b/app/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..d650bd0 Binary files /dev/null and b/app/__pycache__/config.cpython-312.pyc differ diff --git a/app/__pycache__/html_crypto.cpython-312.pyc b/app/__pycache__/html_crypto.cpython-312.pyc new file mode 100644 index 0000000..0f65feb Binary files /dev/null and b/app/__pycache__/html_crypto.cpython-312.pyc differ diff --git a/app/app.py b/app/app.py index c1f0140..940be83 100644 --- a/app/app.py +++ b/app/app.py @@ -37,7 +37,7 @@ app = create_app() def run_app(host='127.0.0.1', port=5123): - init_db() + # init_db() app.run(host=host, port=port, threaded=True, use_reloader=False) diff --git a/app/app_common.py b/app/app_common.py index 67169e0..341da46 100644 --- a/app/app_common.py +++ b/app/app_common.py @@ -14,6 +14,7 @@ from config import mysql_host, mysql_user, mysql_password, mysql_database BASE_DIR = os.path.dirname(os.path.abspath(__file__)) STATIC_DIR = os.path.join(BASE_DIR, 'static') +ASSETS_DIR = os.path.join(BASE_DIR, 'assets') def get_db(): @@ -42,6 +43,22 @@ def _render_html(template_name: str, **context): return render_template_string(content, **context) +def _render_html_new(template_name: str, **context): + """读取 HTML 模板:若为加密文件则先解密,再渲染。未加密或解密失败时按明文渲染。""" + path = os.path.join(BASE_DIR, "web_source", template_name) + if not os.path.isfile(path): + return render_template(template_name, **context) + with open(path, "rb") as f: + raw = f.read() + try: + from html_crypto import decrypt + content = decrypt(raw).decode("utf-8") + except Exception: + content = raw.decode("utf-8", errors="replace") + return render_template_string(content, **context) + + + def _is_session_user_valid(): """校验 session 中的 user_id 是否在数据库中仍存在;不存在则清除 session 并返回 False""" uid = session.get('user_id') diff --git a/app/blueprints/__pycache__/__init__.cpython-312.pyc b/app/blueprints/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..df3d62a Binary files /dev/null and b/app/blueprints/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/blueprints/__pycache__/admin.cpython-312.pyc b/app/blueprints/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..9d093bb Binary files /dev/null and b/app/blueprints/__pycache__/admin.cpython-312.pyc differ diff --git a/app/blueprints/__pycache__/auth.cpython-312.pyc b/app/blueprints/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000..fb68be0 Binary files /dev/null and b/app/blueprints/__pycache__/auth.cpython-312.pyc differ diff --git a/app/blueprints/__pycache__/brand.cpython-312.pyc b/app/blueprints/__pycache__/brand.cpython-312.pyc new file mode 100644 index 0000000..122490f Binary files /dev/null and b/app/blueprints/__pycache__/brand.cpython-312.pyc differ diff --git a/app/blueprints/__pycache__/image.cpython-312.pyc b/app/blueprints/__pycache__/image.cpython-312.pyc new file mode 100644 index 0000000..b5847a3 Binary files /dev/null and b/app/blueprints/__pycache__/image.cpython-312.pyc differ diff --git a/app/blueprints/__pycache__/main.cpython-312.pyc b/app/blueprints/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..43d88ac Binary files /dev/null and b/app/blueprints/__pycache__/main.cpython-312.pyc differ diff --git a/app/blueprints/main.py b/app/blueprints/main.py index 37e63b5..9f3fdf4 100644 --- a/app/blueprints/main.py +++ b/app/blueprints/main.py @@ -2,7 +2,8 @@ 主页面蓝图:首页、home、图片工作台、品牌页、静态文件、Logo """ import os -from flask import Blueprint, send_file +import requests +from flask import Blueprint, Response, request, send_file from app_common import ( get_db, @@ -16,7 +17,9 @@ from app_common import ( ) from flask import redirect, url_for, session -from config import base_url,version +from config import base_url,version,java_api_base + +JAVA_API_BASE = java_api_base main_bp = Blueprint('main', __name__) @@ -90,6 +93,36 @@ def serve_new_web_source(filename): +@main_bp.route('/newApi/', methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']) +def proxy_new_api(subpath): + target_url = f"{JAVA_API_BASE}/{subpath}" + try: + headers = { + key: value + for key, value in request.headers.items() + if key.lower() not in {'host', 'content-length'} + } + upstream = requests.request( + method=request.method, + url=target_url, + params=request.args, + data=request.get_data(), + headers=headers, + cookies=request.cookies, + allow_redirects=False, + timeout=300, + ) + excluded_headers = {'content-encoding', 'content-length', 'transfer-encoding', 'connection'} + response_headers = [ + (name, value) + for name, value in upstream.headers.items() + if name.lower() not in excluded_headers + ] + return Response(upstream.content, upstream.status_code, response_headers) + except requests.RequestException as exc: + return {'success': False, 'message': str(exc), 'data': None}, 502 + + @main_bp.route('/logo.jpg', methods=['GET']) def get_logo_image(): return send_file(os.path.join(BASE_DIR, "logo.jpg"), mimetype='image/jpeg') diff --git a/app/brand_spider/__pycache__/main.cpython-312.pyc b/app/brand_spider/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..161636e Binary files /dev/null and b/app/brand_spider/__pycache__/main.cpython-312.pyc differ diff --git a/app/brand_spider/__pycache__/web_dec.cpython-312.pyc b/app/brand_spider/__pycache__/web_dec.cpython-312.pyc new file mode 100644 index 0000000..13b3b3a Binary files /dev/null and b/app/brand_spider/__pycache__/web_dec.cpython-312.pyc differ diff --git a/app/config.py b/app/config.py index 56b7ef8..503b308 100644 --- a/app/config.py +++ b/app/config.py @@ -7,6 +7,7 @@ import os from dotenv import load_dotenv load_dotenv() _base_url = os.getenv("base_url","http://159.75.121.33:15124") +java_api_base = os.getenv("java_api_base", "http://127.0.0.1:18080") # MySQL 配置 mysql_host = os.getenv("mysql_host") @@ -16,6 +17,7 @@ mysql_database = os.getenv("mysql_database","aiimage") proxy_url = os.getenv("proxy_url") proxy_mode = int(os.getenv("proxy_mode",1)) + client_name=os.getenv("client_name") + ".exe" cache_path = "./user_data" diff --git a/app/main.py b/app/main.py index eb64fdf..517c4ac 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,7 @@ """ 基于 pywebview 实现的跨平台 UI,需先登录方可使用 """ -from config import cache_path,debug,version +from config import cache_path,debug,version,java_api_base import datetime import json import sys @@ -15,6 +15,7 @@ if not debug: import webview import threading import time +import requests with open("version.txt","w",encoding="utf-8") as file: @@ -25,6 +26,7 @@ with open("version.txt","w",encoding="utf-8") as file: BASE_DIR = os.path.dirname(os.path.abspath(__file__)) APP_URL = "http://127.0.0.1:5123" PORT = 5123 +JAVA_API_BASE = java_api_base def generate_images(params): @@ -143,6 +145,28 @@ class WindowAPI: ) return result[0] if result else '' + def upload_file_to_java(self, file_path): + """按本地路径读取文件并上传到 Java 后端临时目录。""" + if not file_path or not str(file_path).strip(): + return {'success': False, 'error': '文件路径为空'} + normalized_path = os.path.abspath(str(file_path).strip()) + if not os.path.isfile(normalized_path): + return {'success': False, 'error': '文件不存在'} + try: + with open(normalized_path, 'rb') as file_obj: + response = requests.post( + f"{JAVA_API_BASE}/api/files/upload", + files={'file': (os.path.basename(normalized_path), file_obj)}, + timeout=120, + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + return {'success': False, 'error': 'Java 返回格式错误'} + return payload + except Exception as e: + return {'success': False, 'error': str(e)} + def save_file_from_url(self, url, default_filename='download.zip'): """弹窗选择保存位置,从 url 下载文件并保存。用于品牌任务结果 zip 等。""" if not url or not url.strip(): @@ -332,7 +356,7 @@ def main(): easy_drag=True ) api = WindowAPI(window) - 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) + 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.upload_file_to_java, api.save_file_from_url, api.save_template_xlsx,api.save_template_zip) webview.start( debug=True, storage_path=cache_path, diff --git a/app/tool/__pycache__/devices.cpython-312.pyc b/app/tool/__pycache__/devices.cpython-312.pyc new file mode 100644 index 0000000..689f9c1 Binary files /dev/null and b/app/tool/__pycache__/devices.cpython-312.pyc differ diff --git a/app/version.txt b/app/version.txt new file mode 100644 index 0000000..fa60655 --- /dev/null +++ b/app/version.txt @@ -0,0 +1 @@ +{"version": "1.0.3"} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nanri/aiimage/config/SecurityConfig.java b/backend-java/src/main/java/com/nanri/aiimage/config/SecurityConfig.java index 968fadf..ec84041 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/config/SecurityConfig.java +++ b/backend-java/src/main/java/com/nanri/aiimage/config/SecurityConfig.java @@ -5,6 +5,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; @Configuration public class SecurityConfig { @@ -12,9 +17,25 @@ public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http + .cors(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) .httpBasic(Customizer.withDefaults()) .build(); } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOriginPatterns(List.of("*")); + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setExposedHeaders(List.of("Content-Disposition")); + configuration.setAllowCredentials(false); + configuration.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/file/controller/FileUploadController.java b/backend-java/src/main/java/com/nanri/aiimage/modules/file/controller/FileUploadController.java index 0f61e6c..210e3f2 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/file/controller/FileUploadController.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/file/controller/FileUploadController.java @@ -1,6 +1,7 @@ package com.nanri.aiimage.modules.file.controller; import com.nanri.aiimage.common.api.ApiResponse; +import com.nanri.aiimage.modules.file.model.vo.ExcelInfoVo; import com.nanri.aiimage.modules.file.model.vo.UploadFileVo; import com.nanri.aiimage.modules.file.service.LocalFileStorageService; import io.swagger.v3.oas.annotations.Operation; @@ -11,8 +12,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -46,4 +49,14 @@ public class FileUploadController { MultipartFile file) throws Exception { return ApiResponse.success(localFileStorageService.saveTempFile(file)); } + + @GetMapping("/excel-info") + @Operation(summary = "读取 Excel 信息", description = "根据 fileKey 读取 Excel 第一行表头及总行数,供前端选择保留列和展示总条数。") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "读取成功", content = @Content(schema = @Schema(implementation = ExcelInfoVo.class))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "源文件不存在") + }) + public ApiResponse excelInfo(@RequestParam String fileKey) throws Exception { + return ApiResponse.success(localFileStorageService.getExcelInfo(fileKey)); + } } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/file/model/vo/ExcelInfoVo.java b/backend-java/src/main/java/com/nanri/aiimage/modules/file/model/vo/ExcelInfoVo.java new file mode 100644 index 0000000..273774c --- /dev/null +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/file/model/vo/ExcelInfoVo.java @@ -0,0 +1,17 @@ +package com.nanri.aiimage.modules.file.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "Excel 文件信息") +public class ExcelInfoVo { + + @Schema(description = "表头列表") + private List headers; + + @Schema(description = "数据总行数") + private Integer totalRows; +} diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/LocalFileStorageService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/LocalFileStorageService.java index 6236bff..ca18468 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/LocalFileStorageService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/file/service/LocalFileStorageService.java @@ -2,14 +2,26 @@ package com.nanri.aiimage.modules.file.service; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.IdUtil; +import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.config.StorageProperties; +import com.nanri.aiimage.modules.file.model.vo.ExcelInfoVo; import com.nanri.aiimage.modules.file.model.vo.UploadFileVo; import lombok.RequiredArgsConstructor; +import org.apache.poi.ss.usermodel.Cell; +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.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Service @RequiredArgsConstructor @@ -36,4 +48,73 @@ public class LocalFileStorageService { vo.setSize(file.getSize()); return vo; } + + public ExcelInfoVo getExcelInfo(String fileKey) throws IOException { + File inputFile = findLocalSourceFile(fileKey); + if (inputFile == null || !inputFile.exists()) { + throw new BusinessException("源文件不存在"); + } + + DataFormatter formatter = new DataFormatter(); + try (FileInputStream fis = new FileInputStream(inputFile); Workbook workbook = org.apache.poi.ss.usermodel.WorkbookFactory.create(fis)) { + Sheet sheet = workbook.getSheetAt(0); + Row headerRow = sheet.getRow(0); + if (headerRow == null) { + throw new BusinessException("Excel 表头为空"); + } + + Map headerMap = new HashMap<>(); + List headers = new ArrayList<>(); + for (int i = 0; i < headerRow.getLastCellNum(); i++) { + Cell cell = headerRow.getCell(i); + String value = normalizeCellText(cell == null ? null : formatter.formatCellValue(cell)); + if (value.isBlank()) { + continue; + } + value = value.split("idASIN国家状态价格变体数量", 2)[0].trim().isEmpty() + ? value + : value.split("idASIN国家状态价格变体数量", 2)[0].trim(); + if (headerMap.containsKey(value)) { + continue; + } + headerMap.put(value, i); + headers.add(value); + if ("缩略图地址8".equals(value)) { + break; + } + } + + ExcelInfoVo vo = new ExcelInfoVo(); + vo.setHeaders(headers); + vo.setTotalRows(Math.max(sheet.getLastRowNum(), 0)); + return vo; + } catch (BusinessException e) { + throw e; + } catch (Exception e) { + throw new BusinessException("读取 Excel 信息失败"); + } + } + + private File findLocalSourceFile(String fileKey) { + File baseDir = FileUtil.file(storageProperties.getLocalTempDir()); + if (!baseDir.exists()) { + return null; + } + List matchedFiles = FileUtil.loopFiles(baseDir, pathname -> pathname.isFile() && pathname.getName().startsWith(fileKey)); + return matchedFiles.isEmpty() ? null : matchedFiles.getFirst(); + } + + private String normalizeCellText(String value) { + if (value == null) { + return ""; + } + return value.replace("\ufeff", "") + .replace("\u3000", " ") + .replace("\r\n", " ") + .replace("\r", " ") + .replace("\n", " ") + .replace("\t", " ") + .trim() + .replaceAll("\\s+", " "); + } } diff --git a/frontend-vue/src/pages/brand/components/BrandConvertTab.vue b/frontend-vue/src/pages/brand/components/BrandConvertTab.vue index 8bbda4b..85d2977 100644 --- a/frontend-vue/src/pages/brand/components/BrandConvertTab.vue +++ b/frontend-vue/src/pages/brand/components/BrandConvertTab.vue @@ -1,6 +1,9 @@