Springboot项目中@GetMapping(“/getAll{arg}”)请求路径中arg不穿值访问

本文探讨了在SpringBoot开发中,如何优化RESTful API的路径参数处理,特别是针对可选路径参数的场景。通过示例展示了如何避免因未传递可选参数导致的404错误,提供了解决方案并比较了不同方法的优劣。

         spring boot 开发restful API时,使用@GetMapping("/getAll/{arg}")方式写请求路径,然后使用@PathVariable 获取arg的值,但如果这个arg不传则报错404提示路劲找不到。想法是这个arg参数是可选的,不传入则是无条件查询,有则是条件查询。代码如下:

@GetMapping("/getAll/{companyName}")
    public ReturnModel getAllBusByCompany(@PathVariable(value="companyName",required=false)String companyName) {
        List<Bus> bus;
        if(""==companyName || null == companyName) {
            bus = automobileRepository.findAll();
        }else {
            bus = automobileRepository.findAllByComName(companyName);
        }
        return Result.success(bus);
    }

问题:使用@PathVariable如何传入可空的路径,并正确mapping到?

答:你现在的接口匹配的路径是:/getAll/{companyName}。
如果不传companyName,那么就会去匹配/getAll,就会报404了。

所以要么分别写成/getAll/getAll/{companyName}两个接口:

@GetMapping("/getAll")
public ReturnModel getAllBus() {
    //findAll...
}

@GetMapping("/getAll/{companyName}")
public ReturnModel getAllBusByCompany(@PathVariable("companyName") String companyName) {
    //findAllByComName...
}

或者写成/getAllBusByCompany?companyName=xxx的形式去调用:

@GetMapping("/getAllBusByCompany")
public ReturnModel getAllBusByCompany(String companyName) {
    //findAll or findAllByComName...
}

 

element-ui.common.js:29286 POST http://localhost:9876/uplodPython 404 (Not Found) upload @ element-ui.common.js:29286 post @ element-ui.common.js:29592 upload @ element-ui.common.js:29520 eval @ element-ui.common.js:29511 uploadFiles @ element-ui.common.js:29509 handleChange @ element-ui.common.js:29490 invokeWithErrorHandling @ vue.runtime.esm.js:3058 invoker @ vue.runtime.esm.js:1859 original_1._wrapper @ vue.runtime.esm.js:7508 index.js??clonedRuleSet-40.use[0]!./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/views/uplodPython.vue?vue&type=script&lang=js:105 Uncaught (in promise) ReferenceError: res is not defined at eval (index.js??clonedRuleSet-40.use[0]!./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/views/uplodPython.vue?vue&type=script&lang=js:105:21) eval @ index.js??clonedRuleSet-40.use[0]!./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/views/uplodPython.vue?vue&type=script&lang=js:105 Promise.then handleFileUpload @ index.js??clonedRuleSet-40.use[0]!./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/views/uplodPython.vue?vue&type=script&lang=js:101 handleError @ element-ui.common.js:29887 onError @ element-ui.common.js:29588 onload @ element-ui.common.js:29267 XMLHttpRequest.send upload @ element-ui.common.js:29286 post @ element-ui.common.js:29592 upload @ element-ui.common.js:29520 eval @ element-ui.common.js:29511 uploadFiles @ element-ui.common.js:29509 handleChange @ element-ui.common.js:29490 invokeWithErrorHandling @ vue.runtime.esm.js:3058 invoker @ vue.runtime.esm.js:1859 original_1._wrapper @ vue.runtime.esm.js:7508 index.js??clonedRuleSet-40.use[0]!./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/views/uplodPython.vue?vue&type=script&lang=js:105 Uncaught (in promise) ReferenceError: res is not defined at eval (index.js??clonedRuleSet-40.use[0]!./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/views/uplodPython.vue?vue&type=script&lang=js:105:21)<template> <div class="classifier-container"> <el-card class="box-card"> <h2 style="text-align: center; color: #1890ff">🐾 动物图像分类器</h2> <p style="text-align: center; color: #666; margin-bottom: 30px"> 支持识别:猫(cat)、狗(dog)、鱼(fish)、大象(elephant) </p> <!-- 图片上区 --> <el-upload class="upload-box" drag action="#" :auto-upload="true" :show-file-list="false" :on-change="handleFileUpload" accept="image/jpeg, image/jpg, image/png" > <i class="el-icon-upload"></i> <div class="el-upload__text"> 将图片拖到此处,或点击选择图片 </div> <div class="el-upload__tip" slot="tip"> 支持 JPG、PNG 格式,大小超过 5MB </div> </el-upload> <!-- 预览与加载状态 --> <div v-if="previewImage" class="preview-section"> <h4>🖼 图像预览</h4> <img :src="previewImage" alt="预览" class="preview-img" /> <div v-if="loading" class="loading-box"> <el-progress type="circle" :percentage="progress" :width="80" ></el-progress> <p style="margin-top: 10px; color: #409eff">推理中...</p> </div> <!-- 预测结果展示 --> <div v-if="result && !loading" class="result-section"> <h4>✅ 识别结果</h4> <el-alert :title="`检测到: ${result.predicted_class} (置信度: ${result.confidence})`" :type="getClassType(result.predicted_class)" show-icon :closable="false" ></el-alert> <!-- 概率分布图表 --> <div class="chart-box"> <h5>📊 各类别概率分布:</h5> <el-bar-chart :data="chartData" /> </div> </div> </div> </el-card> </div> </template> <script> // 简易条形图组件(无需 echarts,使用纯 CSS + Element UI) const ElBarChart = { props: ["data"], render(h) { return h( "div", { style: "margin-top: 10px;" }, this.data.map((item) => h("div", { style: "margin: 12px 0;" }, [ h( "span", { style: "display:inline-block;width:80px;color:#666;" }, item.label ), h("el-progress", { props: { percentage: item.value * 100, strokeWidth: 14, "text-inside": true, status: item.isMax ? "success" : "", }, style: "width: calc(100% - 90px); display:inline-block; vertical-align: middle;", }), ]) ) ); }, }; export default { components: { ElBarChart }, data() { return { previewImage: null, // 预览图片 base64 loading: false, // 是否正在推理 progress: 0, // 进度条 result: null, // 推理返回结果 uploadUrl: "http://localhost:9876/api/uploadPython", // Spring Boot 后端地址 classColors: { cat: "success", dog: "primary", fish: "info", elephant: "warning", }, }; }, computed: { chartData() { if (!this.result || !this.result.all_probabilities) return []; const all = this.result.all_probabilities; const maxClass = this.result.predicted_class; return Object.keys(all).map((label) => ({ label, value: all[label], isMax: label === maxClass, })); }, }, methods: { getClassType(className) { return this.classColors[className] || "info"; }, handleFileUpload(file, fileList) { const allowedTypes = ["image/jpeg", "image/jpg", "image/png"]; const maxSize = 5 * 1024 * 1024; // 5MB if (!allowedTypes.includes(file.raw.type)) { this.$message.error("仅支持 JPG/PNG 格式!"); return; } if (file.size > maxSize) { this.$message.error("图片能超过 5MB!"); return; } // 显示预览 const reader = new FileReader(); reader.onload = (e) => { this.previewImage = e.target.result; }; reader.readAsDataURL(file.raw); // 重置状态 this.loading = true; this.progress = 0; this.result = null; // 模拟进度增长 const interval = setInterval(() => { if (this.progress >= 90) clearInterval(interval); else this.progress += 5; }, 200); // 创建 FormData 发送图片 const formData = new FormData(); formData.append("file", file.raw); // 调用后端 API fetch(this.uploadUrl, { method: "POST", body: formData, }) .then((res) => res.json()) .then((data) => { clearInterval(interval); this.progress = 100; this.loading = false; console.log(res); if (data.success) { this.result = data; this.$message.success(`识别完成:${data.predicted_class}`); } else { this.handleError(data); } }); // .catch((err) => { // clearInterval(interval); // this.loading = false; // this.handleError({ error: res.error }); // }); }, handleError(data) { console.error("推理失败:", data); this.$alert(data.error || "未知错误", "❌ 推理失败", { confirmButtonText: "确定", type: "error", }); }, }, }; </script> <style scoped> .classifier-container { max-width: 800px; margin: 40px auto; padding: 20px; } .box-card { border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .upload-box { width: 100%; margin-bottom: 30px; } .preview-section { margin-top: 20px; text-align: center; } .preview-img { max-width: 100%; max-height: 300px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 20px; } .loading-box { margin: 20px auto; width: fit-content; } .result-section { text-align: left; margin-top: 20px; } .chart-box { margin-top: 20px; background: #f9f9f9; padding: 15px; border-radius: 8px; border: 1px solid #eee; } </style> package com.shop.jieyou.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.shop.jieyou.common.Result; import com.shop.jieyou.entity.UserItem; import com.shop.jieyou.service.ItemService; import com.shop.jieyou.service.PythonService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** * 花卉相关接口控制器 * 提供三大功能: * 1. 获取中国十大名花数据(来自爬虫或缓存) * 2. 手动刷新花卉数据(强制重新爬取) * 3. 基于用户行为的花卉推荐(调用Python协同过滤脚本) */ @RestController @CrossOrigin(origins = "*") // 允许所有域访问,用于前端开发调试(生产环境建议限制域名) @RequestMapping("/api") public class FlowerController { @Autowired private PythonService pythonService; // 注入业务服务层,处理数据获取与推荐逻辑 @Autowired private ItemService itemService; /** * 接口:GET /api/flowers * 功能:获取“中国十大名花”数据列表 * 数据来源:可能来自数据库、Redis 缓存 或 调用 Python 爬虫脚本 * * @return Result<List<Map<String, Object>>> 返回包含花卉信息的成功响应 */ @GetMapping("/flowers") public Result<List<Map<String, Object>>> getTopTenFlowers() { try { // 调用服务层获取花卉数据(内部可能带缓存机制) List<Map<String, Object>> flowers = pythonService.getFlowers(); return Result.success(flowers); // 成功返回数据 } catch (Exception e) { // 捕获异常并统一返回错误码和消息,避免暴露堆栈给前端 return Result.error("500", "获取花卉数据失败:" + e.getMessage()); } } /** * 接口:POST /api/flowers/refresh * 功能:强制刷新花卉数据缓存,触发重新爬取 * 使用场景:管理员手动更新数据时调用 * * @return Result<Map<String, Object>> 返回刷新结果信息 */ @PostMapping("/flowers/refresh") public Result<Map<String, Object>> refreshData() { try { // TODO: 如果实现了 clearCache 方法,请取消注释并调用 // pythonService.clearCache(); // 清除旧缓存,下次 getFlowers 将重新爬取 // 重新获取最新数据(假设此时会触发爬虫) List<Map<String, Object>> flowers = pythonService.getFlowers(); // 构造返回信息 Map<String, Object> data = new HashMap<>(); data.put("message", "数据已刷新"); data.put("count", flowers.size()); return Result.success(data); } catch (Exception e) { return Result.error("500", "刷新失败:" + e.getMessage()); } } // ========== 推荐系统相关常量定义 ========== /** * 输入文件路径:Java 将用户-商品行为数据写入此 JSON 文件供 Python 脚本读取 * 注意:src/main/resources 是编译后打包进 jar 的资源目录,适合运行时写入! * 建议改为外部路径如 "./data/input.json" */ private static final String INPUT_PATH = "src/main/resources/scripts/input.json"; /** * 输出文件路径:Python 脚本将推荐结果写入此文件,Java 再读取返回给前端 */ private static final String OUTPUT_PATH = "src/main/resources/scripts/output.json"; /** * Python 协同过滤脚本路径 * 注意:resources 目录下的 .py 文件在打包后无法直接作为可执行脚本运行 * 更佳做法是将脚本放在项目外部或使用 ProcessBuilder 启动独立服务 */ private static final String PYTHON_SCRIPT = "src/main/resources/scripts/collaborative.py"; /** * 接口:GET /api/recommend?userId=123 * 功能:为指定用户生成个性化花卉推荐列表 * 实现方式:Java 查询数据库 → 写入 JSON 文件 → 调用 Python 脚本计算 → 读取结果返回 * * @param userId 用户ID,必填参数 * @return Result<JsonNode> 推荐的商品ID数组(如 [101, 105, 108]) */ @GetMapping("/recommend") public Result recommendFlowers(@RequestParam("userId") Long userId) { try { // 1. 获取用户行为数据 List<UserItem> matrix = pythonService.getUserItemMatrix(); // 2. 调用 Python 脚本(通过 stdin/stdout 通信) ProcessBuilder pb = new ProcessBuilder("python", PYTHON_SCRIPT, String.valueOf(userId)); pb.redirectErrorStream(true); // 合并错误流 Process process = pb.start(); // 3. 将数据写入脚本的标准输入 ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(process.getOutputStream(), matrix); process.getOutputStream().close(); // 关闭输入,通知Python结束读取 // 4. 读取Python脚本的输出(推荐结果) JsonNode result = mapper.readTree(process.getInputStream()); // 5. 等待脚本执行完毕 int exitCode = process.waitFor(); if (exitCode != 0) { return Result.error("500", "Python script failed with exit code: " + exitCode); } System.out.println(result); return Result.success(result); } catch (Exception e) { e.printStackTrace(); return Result.error("500", "推荐生成失败:" + e.getMessage()); } } @PostMapping("/predict") public ResponseEntity<String> predict(@RequestParam("file") MultipartFile file) { try { // 保存上的文件到临时路径 String tempDir = System.getProperty("java.io.tmpdir"); File tempFile = new File(tempDir, file.getOriginalFilename()); file.transferTo(tempFile); // 调用 Python 脚本执行预测 ProcessBuilder pb = new ProcessBuilder( "D:\\Python\\python.exe", "D:/DevCode/商城/Shop-master/shop-springboot/src/main/resources/scripts/image_classifier.py", "predict", tempFile.getAbsolutePath() ); pb.redirectErrorStream(true); // 合并 stdout 和 stderr Process process = pb.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); StringBuilder output = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { output.append(line); } int exitCode = process.waitFor(); if (exitCode == 0) { return ResponseEntity.ok(output.toString().trim()); } else { return ResponseEntity.status(500).body("{\"error\": \"Prediction failed\"}"); } } catch (Exception e) { return ResponseEntity.status(500).body("{\"error\": \"" + e.getMessage() + "\"}"); } } private static final String PYTHON_EXECUTABLE = "D:\\Python\\python.exe"; // 或 "python3" private static final String INFER_SCRIPT_PATH = "D:/DevCode/商城/Shop-master/shop-springboot/src/main/resources/scripts/python-model/infer.py"; @CrossOrigin(origins = "*") @PostMapping("/uploadPython") public Result<?> classifyImage(@RequestParam("file") MultipartFile file) { ObjectMapper mapper = new ObjectMapper(); // 临时保存上的图片 File tempImage; try { tempImage = File.createTempFile("img_", "_" + file.getOriginalFilename()); file.transferTo(tempImage); } catch (IOException e) { return Result.error("500", "文件保存失败" + e.getMessage()); } try { // 构建命令 ProcessBuilder pb = new ProcessBuilder( PYTHON_EXECUTABLE, INFER_SCRIPT_PATH, tempImage.getAbsolutePath() ); pb.redirectErrorStream(true); // 执行 Process process = pb.start(); // 读取输出 BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); StringBuilder output = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { output.append(line); } // 等待完成 boolean finished = process.waitFor(30, TimeUnit.SECONDS); // 超时保护 if (!finished) { process.destroyForcibly(); return Result.error("500", "推理超时"); } reader.close(); // 清理临时文件 tempImage.delete(); // 解析 JSON String jsonOutput = output.toString().trim(); if (jsonOutput.isEmpty()) { return Result.error("500", "Python 无返回结果"); } JsonNode result = mapper.readTree(jsonOutput); System.out.println( result); return Result.success(result); } catch (InterruptedException | JsonProcessingException e) { return Result.error("500", "解析错误"); } catch (Exception e) { return Result.error("500", "系统错误"+e.getMessage()); } } }# python-model/infer.py import tensorflow as tf import numpy as np import sys import os import json from PIL import Image MODEL_PATH = "animal_classifier.h5" IMG_SIZE = 224 CLASS_NAMES = ['cat', 'dog', 'elephant', 'fish'] # 必须与训练时一致! def predict(image_path): if not os.path.exists(image_path): print(json.dumps({"error": f"图片存在: {image_path}"})) return try: # 加载模型 if not os.path.exists(MODEL_PATH): print(json.dumps({"error": f"模型未到: {MODEL_PATH}"})) return model = tf.keras.models.load_model(MODEL_PATH) # 加载并预处理图像 img = Image.open(image_path).convert("RGB") img = img.resize((IMG_SIZE, IMG_SIZE)) img_array = np.array(img) / 255.0 img_array = np.expand_dims(img_array, axis=0) # 添加 batch 维度 # 预测 preds = model.predict(img_array, verbose=0) confidence = float(np.max(preds)) label_idx = np.argmax(preds) label = CLASS_NAMES[label_idx] result = { "success": True, "predicted_class": label, "confidence": round(confidence, 4), "all_probabilities": { CLASS_NAMES[i]: round(float(preds[0][i]), 4) for i in range(len(CLASS_NAMES)) } } print(json.dumps(result)) except Exception as e: print(json.dumps({"error": f"预测失败: {str(e)}"})) if __name__ == "__main__": if len(sys.argv) != 2: print(json.dumps({"error": "用法: python infer.py <image_path>"})) else: predict(sys.argv[1])
10-22
// 定义该类所在的包路径,遵循 Java 的命名规范(通常是倒置的域名) package com.shop.jieyou.service; // 导入 Jackson 库的核心类,用于将 JSON 字符串解析为 Java 对象 import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; // Spring 框架注解:标识此类为一个服务组件,由 Spring 容器管理生命周期 import com.shop.jieyou.entity.UserItem; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; // Java 标准库导入:用于处理输入输出流、读取外部进程输出 import java.io.*; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Python服务类,用于执行Python爬虫脚本并获取花卉数据 * 每次调用都会直接执行Python脚本,进行任何结果缓存 * * 功能说明: * - 调用位于 resources/scripts/crawler.py 的 Python 爬虫脚本 * - 获取其标准输出(应为 JSON 格式) * - 将 JSON 解析为 List<Map<String, Object>> 结构返回给控制器 * - 使用缓存机制,每次请求均重新运行脚本 */ @Service // 表示这是一个 Spring Service Bean,可被自动扫描并注入到其他组件中 public class PythonService { // 使用 Jackson 提供的 ObjectMapper 实例来序列化/反序列化 JSON 数据 private final ObjectMapper objectMapper = new ObjectMapper(); // 定义 Python 脚本在项目中的相对路径 // 注意:此路径是相对于项目根目录的,适用于开发环境 // 生产环境中可能需要改为绝对路径或通过配置文件指定 private static final String SCRIPT_PATH = "src/main/resources/scripts/crawler.py"; /** * 执行 Python 爬虫脚本以获取花卉信息列表 * * 此方法会: * 1. 启动一个新的操作系统进程来运行 Python 脚本 * 2. 捕获脚本的标准输出 * 3. 验证执行状态(退出码) * 4. 解析输出为 Java 对象 * 5. 返回结构化数据 * * @return 包含花卉信息的 Map 列表,每个 Map 表示一种花卉的字段(如 name, family 等) * @throws IOException 当发生 I/O 错误(如无法启动进程、读取输出失败)时抛出 * @throws InterruptedException 当当前线程在等待进程结束时被中断,通常发生在 JVM 关闭期间 */ public synchronized List<Map<String, Object>> getFlowers() throws IOException, InterruptedException { // 创建一个 ProcessBuilder 实例,用于构建和启动外部进程 // 参数:"python" 是命令,SCRIPT_PATH 是要执行的脚本路径 ProcessBuilder pb = new ProcessBuilder("python", SCRIPT_PATH); // 设置合并错误流到标准输出流 // 这样可以通过同一个 BufferedReader 同时读取正常输出和错误信息 // 便于调试问题(例如 Python 报错 ImportError) pb.redirectErrorStream(true); // 启动外部进程(即运行 python crawler.py) // process 对象可用于控制该进程(等待、杀死等) Process process = pb.start(); // 使用 try-with-resources 确保 BufferedReader 在使用后自动关闭 // InputStreamReader 将字节流转换为字符流,并指定 UTF-8 编码以正确处理中文 try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { // 用于拼接从 Python 脚本输出的所有文本行 StringBuilder output = new StringBuilder(); String line; // 循环读取每行输出,直到流结束(EOF) while ((line = reader.readLine()) != null) { // 去除每行首尾空白(包括换行符、空格),然后追加到总输出中 // 注意:这里没有添加换行符,意味着所有内容会被压缩成一行 output.append(line.trim()); } // 等待 Python 进程执行完成,并获取其退出码 // 正常情况下应返回 0;非零表示异常退出(如语法错误、模块未安装) int exitCode = process.waitFor(); // 如果退出码是 0,说明脚本执行失败 if (exitCode != 0) { throw new RuntimeException("Python script exited with code: " + exitCode); } // 检查输出是否为空 // 即使脚本成功退出,也可能未打印任何有效数据 if (output.length() == 0) { throw new RuntimeException("Python script returned empty output"); } // 使用 Jackson 反序列化 JSON 字符串为 Java 对象 // TypeReference 是泛型辅助类,告诉 ObjectMapper 我们想要的是 List<Map<String, Object>> // 每个 Map 对应一条花卉记录,key 是字段名(如 "name"),value 是对应 List<Map<String, Object>> result = objectMapper.readValue(output.toString(), new TypeReference<List<Map<String, Object>>>() {}); // 检查返回的数据中是否包含错误信息(假设 Python 脚本约定第一个元素带 error 字段表示失败) if (!result.isEmpty() && result.get(0).containsKey("error")) { throw new RuntimeException("爬虫错误: " + result.get(0).get("error")); } // 成功解析并验证后,返回花卉数据列表 return result; } catch (Exception e) { // 异常处理:确保即使出错也能清理系统资源 // 如果进程仍在运行,则强制终止它,防止僵尸进程或资源泄漏 if (process.isAlive()) { process.destroyForcibly(); } // 继续向上抛出异常,让调用者知道发生了什么 throw e; } } @Autowired JdbcTemplate jdbcTemplate; public List<UserItem> getUserItemMatrix() { String sql = "SELECT user_id, product_id, COUNT(*) as count " + "FROM tb_order WHERE state = 1 " + "GROUP BY user_id, product_id"; return jdbcTemplate.query(sql, (rs, rowNum) -> new UserItem( rs.getLong("user_id"), rs.getLong("product_id"), rs.getInt("count") ) ); } private static final String PYTHON_SCRIPT_PATH = "src/main/resources/scripts/python-model/infer.py"; private static final String PYTHON_ENV_PATH = "python"; // 虚拟环境 public Map<String, Object> classify(String file) throws IOException, InterruptedException, Exception { // 1. 校验文件类型(安全) // if (!"image/jpeg".equals(file) && !"image/png".equals(file)) { // throw new IllegalArgumentException("仅支持 JPG/PNG 图片"); // } // 2. 保存临时文件 File tempFile = File.createTempFile("upload_", ".jpg"); try (FileOutputStream fos = new FileOutputStream(tempFile)) { fos.write(file.getBytes()); } // 3. 调用 Python 脚本 ProcessBuilder pb = new ProcessBuilder( PYTHON_ENV_PATH, PYTHON_SCRIPT_PATH, tempFile.getAbsolutePath() ); pb.redirectErrorStream(true); Process process = pb.start(); // 4. 获取输出结果 StringBuilder output = new StringBuilder(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { output.append(line); } } int exitCode = process.waitFor(); if (exitCode != 0) { throw new RuntimeException("Python 脚本执行失败,退出码: " + exitCode); } // 5. 解析 JSON 结果(假设 Python 返回的是 JSON 字符串) ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readTree(output.toString().trim()); Map<String, Object> result = new HashMap<>(); result.put("predicted_class", jsonNode.get("predicted_class").asText()); result.put("confidence", jsonNode.get("confidence").asDouble()); return result; } } # python-model/infer.py import numpy as np import sys import os import json import logging from PIL import Image os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" logging.getLogger("tensorflow").setLevel(logging.FATAL) import tensorflow as tf MODEL_PATH = "animal_classifier.h5" IMG_SIZE = 224 CLASS_NAMES = ['cat', 'dog', 'elephant', 'fish'] # 必须与训练时一致! def predict(image_path): if not os.path.exists(image_path): print(json.dumps({"error": f"图片存在: {image_path}"})) return try: # 加载模型 if not os.path.exists(MODEL_PATH): print(json.dumps({"error": f"模型未到: {MODEL_PATH}"})) return model = tf.keras.models.load_model(MODEL_PATH) # 加载并预处理图像 img = Image.open(image_path).convert("RGB") img = img.resize((IMG_SIZE, IMG_SIZE)) img_array = np.array(img) / 255.0 img_array = np.expand_dims(img_array, axis=0) # 添加 batch 维度 # 预测 preds = model.predict(img_array, verbose=0) confidence = float(np.max(preds)) label_idx = np.argmax(preds) label = CLASS_NAMES[label_idx] result = { "success": True, "predicted_class": label, "confidence": round(confidence, 4), "all_probabilities": { CLASS_NAMES[i]: round(float(preds[0][i]), 4) for i in range(len(CLASS_NAMES)) } } print(json.dumps(result)) except Exception as e: print(json.dumps({"error": f"预测失败: {str(e)}"})) if __name__ == "__main__": if len(sys.argv) != 2: print(json.dumps({"error": "用法: python infer.py <image_path>"})) else:package com.shop.jieyou.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.shop.jieyou.common.Result; import com.shop.jieyou.entity.UserItem; import com.shop.jieyou.service.ItemService; import com.shop.jieyou.service.PythonService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; /** * 花卉相关接口控制器 * 提供三大功能: * 1. 获取中国十大名花数据(来自爬虫或缓存) * 2. 手动刷新花卉数据(强制重新爬取) * 3. 基于用户行为的花卉推荐(调用Python协同过滤脚本) */ @RestController @CrossOrigin(origins = "*") // 允许所有域访问,用于前端开发调试(生产环境建议限制域名) @RequestMapping("/api") public class FlowerController { @Autowired private PythonService pythonService; // 注入业务服务层,处理数据获取与推荐逻辑 @Autowired private ItemService itemService; /** * 接口:GET /api/flowers * 功能:获取“中国十大名花”数据列表 * 数据来源:可能来自数据库、Redis 缓存 或 调用 Python 爬虫脚本 * * @return Result<List<Map<String, Object>>> 返回包含花卉信息的成功响应 */ @GetMapping("/flowers") public Result<List<Map<String, Object>>> getTopTenFlowers() { try { // 调用服务层获取花卉数据(内部可能带缓存机制) List<Map<String, Object>> flowers = pythonService.getFlowers(); return Result.success(flowers); // 成功返回数据 } catch (Exception e) { // 捕获异常并统一返回错误码和消息,避免暴露堆栈给前端 return Result.error("500", "获取花卉数据失败:" + e.getMessage()); } } /** * 接口:POST /api/flowers/refresh * 功能:强制刷新花卉数据缓存,触发重新爬取 * 使用场景:管理员手动更新数据时调用 * * @return Result<Map<String, Object>> 返回刷新结果信息 */ @PostMapping("/flowers/refresh") public Result<Map<String, Object>> refreshData() { try { // TODO: 如果实现了 clearCache 方法,请取消注释并调用 // pythonService.clearCache(); // 清除旧缓存,下次 getFlowers 将重新爬取 // 重新获取最新数据(假设此时会触发爬虫) List<Map<String, Object>> flowers = pythonService.getFlowers(); // 构造返回信息 Map<String, Object> data = new HashMap<>(); data.put("message", "数据已刷新"); data.put("count", flowers.size()); return Result.success(data); } catch (Exception e) { return Result.error("500", "刷新失败:" + e.getMessage()); } } // ========== 推荐系统相关常量定义 ========== /** * 输入文件路径:Java 将用户-商品行为数据写入此 JSON 文件供 Python 脚本读取 * 注意:src/main/resources 是编译后打包进 jar 的资源目录,适合运行时写入! * 建议改为外部路径如 "./data/input.json" */ private static final String INPUT_PATH = "src/main/resources/scripts/input.json"; /** * 输出文件路径:Python 脚本将推荐结果写入此文件,Java 再读取返回给前端 */ private static final String OUTPUT_PATH = "src/main/resources/scripts/output.json"; /** * Python 协同过滤脚本路径 * 注意:resources 目录下的 .py 文件在打包后无法直接作为可执行脚本运行 * 更佳做法是将脚本放在项目外部或使用 ProcessBuilder 启动独立服务 */ private static final String PYTHON_SCRIPT = "src/main/resources/scripts/collaborative.py"; /** * 接口:GET /api/recommend?userId=123 * 功能:为指定用户生成个性化花卉推荐列表 * 实现方式:Java 查询数据库 → 写入 JSON 文件 → 调用 Python 脚本计算 → 读取结果返回 * * @param userId 用户ID,必填参数 * @return Result<JsonNode> 推荐的商品ID数组(如 [101, 105, 108]) */ @GetMapping("/recommend") public Result recommendFlowers(@RequestParam("userId") Long userId) { try { // 1. 获取用户行为数据 List<UserItem> matrix = pythonService.getUserItemMatrix(); // 2. 调用 Python 脚本(通过 stdin/stdout 通信) ProcessBuilder pb = new ProcessBuilder("python", PYTHON_SCRIPT, String.valueOf(userId)); pb.redirectErrorStream(true); // 合并错误流 Process process = pb.start(); // 3. 将数据写入脚本的标准输入 ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(process.getOutputStream(), matrix); process.getOutputStream().close(); // 关闭输入,通知Python结束读取 // 4. 读取Python脚本的输出(推荐结果) JsonNode result = mapper.readTree(process.getInputStream()); // 5. 等待脚本执行完毕 int exitCode = process.waitFor(); if (exitCode != 0) { return Result.error("500", "Python script failed with exit code: " + exitCode); } System.out.println(result); return Result.success(result); } catch (Exception e) { e.printStackTrace(); return Result.error("500", "推荐生成失败:" + e.getMessage()); } } @PostMapping("/predict") public ResponseEntity<String> predict(@RequestParam("file") MultipartFile file) { try { // 保存上的文件到临时路径 String tempDir = System.getProperty("java.io.tmpdir"); File tempFile = new File(tempDir, file.getOriginalFilename()); file.transferTo(tempFile); // 调用 Python 脚本执行预测 ProcessBuilder pb = new ProcessBuilder( "D:\\Python\\python.exe", "D:/DevCode/商城/Shop-master/shop-springboot/src/main/resources/scripts/image_classifier.py", "predict", tempFile.getAbsolutePath() ); pb.redirectErrorStream(true); // 合并 stdout 和 stderr Process process = pb.start(); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); StringBuilder output = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { output.append(line); } int exitCode = process.waitFor(); if (exitCode == 0) { return ResponseEntity.ok(output.toString().trim()); } else { return ResponseEntity.status(500).body("{\"error\": \"Prediction failed\"}"); } } catch (Exception e) { return ResponseEntity.status(500).body("{\"error\": \"" + e.getMessage() + "\"}"); } } private static final String PYTHON_EXECUTABLE = "D:\\Python\\python.exe"; // 或 "python3" private static final String INFER_SCRIPT_PATH = "D:/DevCode/商城/Shop-master/shop-springboot/src/main/resources/scripts/python-model/infer.py"; @PostMapping("/uploadPython") public Result<Map<String, Object>> classifyImage(@RequestParam String file) { if (file.isEmpty()) { return Result.error("400", "文件为空"); } try { // 只做参数递,逻辑交给 Service Map<String, Object> result = pythonService.classify(file); return Result.success(result); } catch (IOException e) { return Result.error("500", "文件处理失败:" + e.getMessage()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return Result.error("500", "处理被中断"); } catch (Exception e) { return Result.error("500", "识别出错:" + e.getMessage()); } } } predict(sys.argv[1])
最新发布
10-22
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值