6.3冲刺

昨天的成就: 完成了AI识别控制器开发,实现了单张/批量图片识别接口,集成了进度跟踪模块的AI功能,编写了使用文档,耗时11小时
遇到的困难: 批量识别并发控制导致部分失败,AI结果存储结构设计复杂,前端展示杂乱,大图片API超时
今天的任务: 功能测试与Bug修复,优化提示词工程,添加识别历史记录功能
📝 工作代码展示

  1. AI识别控制器 (backend/src/controllers/aiController.js)
    javascript
    const express = require('express');
    const multer = require('multer');
    const aiService = require('../services/aiService');
    const router = express.Router();

// 配置Multer:内存存储 + 10MB限制
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
// 模糊照片检测:小于2KB视为无效
if (file.size < 2048) {
return cb(new Error('图片过于模糊或文件损坏'), false);
}
cb(null, true);
} else {
cb(new Error('只支持图片格式'), false);
}
}
});

/**

  • 单张图片识别

  • POST /api/progress/recognize
    */
    router.post('/recognize', upload.single('image'), async (req, res) => {
    try {
    if (!req.file) throw new Error('未上传图片');

    const result = await aiService.recognizeImage(
    req.file.buffer,
    req.file.mimetype,
    req.file.originalname
    );

    res.json({ success: true, data: result });
    } catch (error) {
    // 自动清理机制已在aiService中处理(内存存储无需手动删除)
    res.status(500).json({ success: false, message: error.message });
    }
    });

/**

  • 批量图片识别(最多10张)

  • POST /api/progress/recognize-multiple
    */
    router.post('/recognize-multiple', upload.array('images', 10), async (req, res) => {
    try {
    if (!req.files || req.files.length === 0) {
    throw new Error('未上传图片');
    }

    // 使用 Promise.allSettled 确保单张失败不影响其他
    const tasks = req.files.map(file =>
    aiService.recognizeImage(file.buffer, file.mimetype, file.originalname)
    .then(result => ({ ...result, fileName: file.originalname }))
    .catch(err => ({
    success: false,
    fileName: file.originalname,
    error: err.message
    }))
    );

    const results = await Promise.allSettled(tasks);

    // 提取最终结果
    const processedResults = results.map(r => r.value);

    res.json({
    success: true,
    total: req.files.length,
    successCount: processedResults.filter(r => r.success).length,
    data: processedResults
    });

} catch (error) {
res.status(500).json({ success: false, message: '批量识别失败' });
}
});

module.exports = router;
2. 进度跟踪模块集成 (backend/src/controllers/progressController.js 修改部分)
javascript
const aiService = require('../services/aiService');
const ConstructionProgress = require('../models/ConstructionProgress');

// 在原有的上传照片方法中集成AI识别
exports.uploadPhoto = async (req, res) => {
try {
const { projectId, stageId } = req.body;
const photoUrl = req.file.path; // 假设已上传到OSS或本地存储

// 1. 保存进度记录
const progress = await ConstructionProgress.create({
projectId,
stageId,
photoUrl,
uploadTime: new Date()
});

// 2. 异步调用AI识别(不阻塞主流程)
// 注意:生产环境建议使用消息队列(如RabbitMQ)处理异步任务
processAIRecognition(progress.id, req.file.buffer, req.file.mimetype);

res.json({
success: true,
message: '照片上传成功,AI识别进行中...',
data: progress
});

} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};

// 独立的AI识别处理函数
async function processAIRecognition(progressId, imageBuffer, mimeType) {
try {
console.log([AI Task] 开始识别进度ID: ${progressId});

// 调用AI服务
const aiResult = await aiService.recognizeImage(imageBuffer, mimeType, progress_${progressId}.jpg);

// 将结构化结果存入 review_remark 字段
await ConstructionProgress.update(
{ reviewRemark: JSON.stringify(aiResult.structured) },
{ where: { id: progressId } }
);

console.log([AI Task] 识别完成并保存: ${progressId});

} catch (error) {
console.error([AI Task] 识别失败 ID: ${progressId}, error.message);
// 可选:记录错误日志表
}
}
3. 前端展示优化 (frontend/src/pages/ProgressTracking.js 修改部分)
jsx
import React, { useState } from 'react';
import { Modal, Card, Tag, Button, Spin } from 'antd';
import { WarningOutlined, CheckCircleOutlined, BulbOutlined } from '@ant-design/icons';

const ProgressTracking = () => {
const [aiModalVisible, setAiModalVisible] = useState(false);
const [aiData, setAiData] = useState(null);
const [loading, setLoading] = useState(false);

// 获取AI结果并显示
const showAIResult = async (progressId) => {
setLoading(true);
try {
// 假设已有API获取该进度的AI结果
const response = await fetch(/api/progress/${progressId}/ai-result);
const data = await response.json();

if (data.success) {
setAiData(JSON.parse(data.data.reviewRemark));
setAiModalVisible(true);
} else {
message.error('暂无AI识别结果');
}
} catch (err) {
message.error('获取AI结果失败');
} finally {
setLoading(false);
}
};

return (

{/* AI结果弹窗 */}
<Modal
title="🤖 AI 智能分析报告"
visible={aiModalVisible}
onCancel={() => setAiModalVisible(false)}
footer={[
<Button key="copy" onClick={() => copyToClipboard(aiData)}>复制结果,
<Button key="close" type="primary" onClick={() => setAiModalVisible(false)}>关闭
]}
width={700}
>

{aiData && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>

{/* 安全风险卡片 */}
<Card title={<WarningOutlined style={{color: '#ff4d4f'}} /> 安全风险} bordered={false} size="small">
{aiData.safetyRisks?.length > 0 ? (
aiData.safetyRisks.map((risk, idx) => (
<Tag color="red" key={idx} style={{ marginBottom: '8px', display: 'block' }}>
{risk}

))
) : (
未发现明显安全风险
)}

{/* 施工质量卡片 */}
<Card title={<CheckCircleOutlined style={{color: '#52c41a'}} /> 施工质量} bordered={false} size="small">
{aiData.qualityIssues?.length > 0 ? (
aiData.qualityIssues.map((issue, idx) => (
<Tag color="orange" key={idx} style={{ marginBottom: '8px', display: 'block' }}>
{issue}

))
) : (
施工质量符合规范
)}

{/* 改进建议卡片 */}
<Card title={<BulbOutlined style={{color: '#faad14'}} /> 改进建议} bordered={false} size="small">
{aiData.suggestions?.map((sug, idx) => (
<p key={idx} style={{ margin: '4px 0', fontSize: '14px' }}>• {sug}


))}


)}



);
};

export default ProgressTracking;
4. 前端图片压缩工具 (frontend/src/utils/imageCompressor.js)
javascript
/**

  • 图片压缩工具

  • @param {File} file - 原始文件

  • @param {number} maxWidth - 最大宽度

  • @param {number} maxHeight - 最大高度

  • @param {number} quality - 压缩质量 (0-1)
    */
    export const compressImage = (file, maxWidth = 1920, maxHeight = 1080, quality = 0.8) => {
    return new Promise((resolve) => {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const img = new Image();

    img.onload = () => {
    // 计算缩放比例,保持宽高比
    let width = img.width;
    let height = img.height;

    if (width > height) {
    if (width > maxWidth) {
    height *= maxWidth / width;
    width = maxWidth;
    }
    } else {
    if (height > maxHeight) {
    width *= maxHeight / height;
    height = maxHeight;
    }
    }

    canvas.width = width;
    canvas.height = height;

    // 绘制图片
    ctx.drawImage(img, 0, 0, width, height);

    // 转换为Blob
    canvas.toBlob((blob) => {
    resolve(new File([blob], file.name, {
    type: 'image/jpeg',
    lastModified: Date.now()
    }));
    }, 'image/jpeg', quality);
    };

    img.src = URL.createObjectURL(file);
    });
    };

posted @ 2026-06-18 21:28  sas1996  阅读(8)  评论(0)    收藏  举报