const express = require('express');
const cors = require('cors');
const multer = require('multer');
const axios = require('axios');
const fs = require('fs');
const qs = require('qs');
const path = require('path');
const app = express();
const port = 3000;
// 创建上传目录
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir);
}
// 跨域配置
app.use(cors({
origin: '*',
methods: ['POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// 文件上传配置
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/bmp', 'image/gif'];
if (!allowedTypes.includes(file.mimetype)) {
return cb(new Error('仅支持JPEG/PNG/BMP/GIF格式图片'), false);
}
cb(null, true);
},
limits: {
fileSize: 5 * 1024 * 1024 // 限制5MB
}
});
// 百度API配置(请替换为实际密钥)
const BAIDU_API_KEY = 'iBhL0RP4TD0ioSFbWI55Hs90';
const BAIDU_SECRET_KEY = '9pLRd6YNyq7NLytHMOBLACNSO80CQtmE';
const TOKEN_URL = 'https://aip.baidubce.com/oauth/2.0/token';
const PLANT_URL = 'https://aip.baidubce.com/rest/2.0/image-classify/v1/plant';
// 全局错误处理
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
return res.status(400).json({
error: `文件上传错误: ${err.message}`,
result: []
});
} else if (err) {
return res.status(500).json({
error: `服务器错误: ${err.message}`,
result: []
});
}
next();
});
// 植物识别路由
app.post('/recognize', upload.single('image'), async (req, res) => {
let tempFilePath = '';
try {
if (!req.file) {
return res.status(400).json({
error: '请上传有效的图片文件',
result: []
});
}
tempFilePath = req.file.path;
// 读取图片并转换为base64
const imageBuffer = fs.readFileSync(tempFilePath);
const imageBase64 = imageBuffer.toString('base64');
// 获取百度API访问令牌
const tokenRes = await axios.post(TOKEN_URL, null, {
params: {
grant_type: 'client_credentials',
client_id: BAIDU_API_KEY,
client_secret: BAIDU_SECRET_KEY
}
});
if (!tokenRes.data || !tokenRes.data.access_token) {
throw new Error('获取百度API访问令牌失败');
}
const accessToken = tokenRes.data.access_token;
// 调用植物识别API
const plantRes = await axios.post(
PLANT_URL,
qs.stringify({
image: imageBase64,
baike_num: 5 // 返回5个结果
}),
{
params: { access_token: accessToken },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
timeout: 20000 // 20秒超时
}
);
console.log('百度API原始响应:', plantRes.data);
// 统一响应格式 - 更健壮的处理
let resultData = [];
// 情况1:标准格式 { result: [...] }
if (plantRes.data && Array.isArray(plantRes.data.result)) {
resultData = plantRes.data.result;
}
// 情况2:百度API错误格式(如返回错误码)
else if (plantRes.data.error_code) {
throw new Error(`百度API错误: ${plantRes.data.error_msg} (错误码: ${plantRes.data.error_code})`);
}
// 情况3:直接数组格式(理论上百度API不会这样返回,但为了健壮性保留)
else if (Array.isArray(plantRes.data)) {
resultData = plantRes.data;
}
// 情况4:其他未知格式
else {
throw new Error('百度API返回了无法解析的格式');
}
// 格式化响应
const formattedResponse = {
result: resultData
.filter(item => item.name) // 过滤无效项(必须有名字)
.map(item => ({
name: item.name,
score: item.score || 0 // 如果没有分数,设为0
}))
};
// 如果过滤后没有结果,尝试使用第一个结果(即使没有名字,但可能有其他信息)
if (formattedResponse.result.length === 0 && resultData.length > 0) {
formattedResponse.result = [{
name: resultData[0].name || '未知植物',
score: resultData[0].score || 0
}];
}
console.log('格式化后的响应:', formattedResponse);
res.json(formattedResponse);
} catch (error) {
console.error('识别错误:', error);
let errorMessage = '植物识别失败';
// 提取更具体的错误信息
if (error.response) {
// 如果是百度API的响应错误
if (error.response.data && error.response.data.error_msg) {
errorMessage = `百度API错误: ${error.response.data.error_msg}`;
} else {
errorMessage = `服务请求错误: ${error.response.status} ${error.response.statusText}`;
}
} else if (error.code === 'ECONNABORTED') {
errorMessage = '请求超时,请重试';
} else {
errorMessage = error.message;
}
res.status(500).json({
error: errorMessage,
result: []
});
} finally {
// 清理临时文件
if (tempFilePath) {
fs.unlink(tempFilePath, (err) => {
if (err) console.error('删除临时文件失败:', err);
});
}
}
});
// 启动服务器
app.listen(port, () => {
console.log(`植物识别服务运行中:http://localhost:${port}`);
console.log('等待上传图片进行识别...');
}); <template>
<div class="plant-recognize-page">
<van-nav-bar title="植物识别" />
<div class="upload-container">
<div class="uploader-wrapper">
<van-uploader
:after-read="onRead"
accept="image/*"
:preview-size="120"
:max-count="1"
v-model="fileList"
>
<van-button type="primary" block round icon="photo">选择图片</van-button>
</van-uploader>
</div>
<div v-if="uploadedImage" class="preview-container">
<van-image
:src="uploadedImage"
fit="contain"
width="100%"
height="auto"
class="preview-image"
/>
</div>
</div>
<div v-if="loading" class="loading-overlay">
<van-loading size="24px">识别中,请稍候...</van-loading>
</div>
<div v-if="results.length > 0" class="result-section">
<h3 class="result-title">识别结果:</h3>
<van-cell-group inset>
<van-cell
v-for="(item, index) in results"
:key="index"
:title="item.name"
:value="`${(item.score * 100).toFixed(2)}%`"
:label="`置信度:${(item.score * 100).toFixed(2)}%`"
/>
</van-cell-group>
</div>
<div v-else-if="errorMessage" class="error-message">
<van-icon name="warning-o" color="#f56c6c" size="16" />
<span>{{ errorMessage }}</span>
</div>
</div>
</template>
<script>
import axios from 'axios';
import {
NavBar, Uploader, Image, Button, Loading,
Cell, CellGroup, Toast, Icon
} from 'vant';
export default {
name: 'PlantRecognize',
components: {
[NavBar.name]: NavBar,
[Uploader.name]: Uploader,
[Image.name]: Image,
[Button.name]: Button,
[Loading.name]: Loading,
[Cell.name]: Cell,
[CellGroup.name]: CellGroup,
[Icon.name]: Icon
},
data() {
return {
fileList: [],
uploadedImage: '',
loading: false,
results: [],
errorMessage: ''
};
},
methods: {
async onRead(file) {
this.loading = true;
this.results = [];
this.errorMessage = '';
try {
// 预览图片
const reader = new FileReader();
reader.onload = (e) => {
this.uploadedImage = e.target.result;
};
reader.readAsDataURL(file.file);
// 准备上传数据
const formData = new FormData();
formData.append('image', file.file);
// 发送识别请求
const response = await axios.post(
'http://localhost:3000/recognize',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 30000 // 30秒超时
}
);
console.log('后端原始响应数据:', response.data);
// 更健壮的响应格式校验
if (response.data &&
typeof response.data === 'object' &&
Array.isArray(response.data.result)) {
// 转换格式,添加默认值处理
this.results = response.data.result.map(item => ({
name: item.name || '未知植物',
score: item.score || 0
}));
if (this.results.length === 0) {
this.errorMessage = '未识别到植物特征,请尝试其他图片';
Toast.info(this.errorMessage);
}
}
// 处理百度API原始响应格式(直接数组)
else if (response.data && Array.isArray(response.data)) {
this.results = response.data
.filter(item => item.name) // 过滤掉无效项
.map(item => ({
name: item.name,
score: item.score || 0
}));
Toast.success('识别成功');
}
else {
throw new Error('服务端响应格式异常,缺少有效数据');
}
} catch (error) {
console.error('识别失败:', error);
// 更详细的错误处理
if (error.response) {
// 尝试获取后端返回的错误信息
if (error.response.data && error.response.data.error) {
this.errorMessage = error.response.data.error;
} else {
this.errorMessage = `服务端错误: ${error.response.status} ${error.response.statusText}`;
}
} else if (error.request) {
this.errorMessage = '网络请求无响应,请检查服务是否运行';
} else {
this.errorMessage = error.message || '识别失败,请重试';
}
Toast.fail(this.errorMessage);
} finally {
this.loading = false;
}
}
}
};
</script>
<style scoped>
.plant-recognize-page {
min-height: 100vh;
background-color: #f7f8fa;
padding-bottom: 20px;
}
.upload-container {
padding: 20px;
background: white;
margin: 20px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
.uploader-wrapper {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom: 20px;
}
.preview-container {
margin-top: 20px;
text-align: center;
border: 1px dashed #ebedf0;
border-radius: 8px;
padding: 10px;
background: #fafafa;
}
.preview-image {
max-height: 300px;
display: block;
margin: 0 auto;
}
.loading-overlay {
margin: 30px 0;
text-align: center;
color: #969799;
}
.result-section {
margin: 20px;
padding: 16px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
.result-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 12px;
color: #323233;
padding-left: 8px;
}
.error-message {
margin: 20px;
padding: 12px 16px;
text-align: center;
color: #f56c6c;
background-color: #fef0f0;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
}未识别到有效数据
.error-message .van-icon {
margin-right: 8px;
}
</style>