https://github.com/vuejs/vue-devtools
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
uplodPython:1 Access to fetch at 'http://localhost:9090/api/api/uploadPython' from origin 'http://localhost:9876' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
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:98
POST http://localhost:9090/api/api/uploadPython net::ERR_FAILED 200 (OK)
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:98
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:98
Uncaught (in promise) TypeError: Failed to fetch
at VueComponent.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:98:7)
at VueComponent.handleError (element-ui.common.js:29887:12)
at Object.onError (element-ui.common.js:29588:18)
at XMLHttpRequest.onload (element-ui.common.js:29267:21)
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:98
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
uplodPython:1 Access to fetch at 'http://localhost:9090/api/api/uploadPython' from origin 'http://localhost:9876' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
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:98
POST http://localhost:9090/api/api/uploadPython net::ERR_FAILED 200 (OK)
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:98
handleStart @ element-ui.common.js:29859
eval @ element-ui.common.js:29510
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:98
Uncaught (in promise) TypeError: Failed to fetch
at VueComponent.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:98:7)
at VueComponent.handleStart (element-ui.common.js:29859:12)
at eval (element-ui.common.js:29510:15)
at Array.forEach (<anonymous>)
at VueComponent.uploadFiles (element-ui.common.js:29509:17)
at VueComponent.handleChange (element-ui.common.js:29490:12)
at invokeWithErrorHandling (vue.runtime.esm.js:3058:30)
at HTMLInputElement.invoker (vue.runtime.esm.js:1859:20)
at original_1._wrapper (vue.runtime.esm.js:7508:35)<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:9090/api/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: "网络错误或服务未启动" });
// });
},
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);
return Result.success(result);
} catch (InterruptedException | JsonProcessingException e) {
return Result.error("500", "解析错误");
} catch (Exception e) {
return Result.error("500", "系统错误"+e.getMessage());
}
}
}
最新发布