6.5冲刺
昨天的成就: 完成了AI识别历史记录功能的全栈开发,实现了Excel/PDF报告导出功能,编写并执行了团队培训,耗时10小时
遇到的困难: Excel图片插入定位复杂,PDF中文显示乱码,大批量导出内存溢出,现场演示API故障
今天的任务: Sprint 3功能验收,修复验收问题,编写总结报告,部署到测试环境
工作代码展示
- AI识别历史记录数据库模型 (backend/src/models/AIRecognitionLog.js)
javascript
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const AIRecognitionLog = sequelize.define('AIRecognitionLog', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
applicationId: {
type: DataTypes.STRING,
allowNull: false,
comment: '关联申请ID'
},
stage: {
type: DataTypes.STRING,
comment: '施工阶段'
},
imagePath: {
type: DataTypes.STRING,
allowNull: false,
comment: '图片存储路径'
},
fileSize: {
type: DataTypes.BIGINT,
comment: '文件大小(字节)'
},
riskLevel: {
type: DataTypes.ENUM('low', 'medium', 'high'),
defaultValue: 'low',
comment: '风险等级'
},
safetyRisksCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '安全风险数量'
},
qualityIssuesCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '质量问题数量'
},
suggestionsCount: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '改进建议数量'
},
recognitionTime: {
type: DataTypes.INTEGER,
comment: '识别耗时(毫秒)'
},
success: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: '是否成功'
},
errorMessage: {
type: DataTypes.TEXT,
comment: '错误信息'
},
createdAt: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
}
}, {
tableName: 'ai_recognition_logs',
timestamps: false
});
module.exports = AIRecognitionLog;
2. Excel导出功能 (backend/src/services/exportService.js)
javascript
const ExcelJS = require('exceljs');
const fs = require('fs');
const path = require('path');
class ExportService {
/**
- 导出AI识别记录为Excel
- @param {Array} records - 识别记录数组
- @param {string} outputPath - 输出文件路径
*/
async exportToExcel(records, outputPath) {
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('AI识别记录');
// 定义列
worksheet.columns = [
{ header: '序号', key: 'id', width: 8 },
{ header: '图片', key: 'image', width: 35 },
{ header: '施工阶段', key: 'stage', width: 15 },
{ header: '风险等级', key: 'riskLevel', width: 12 },
{ header: '安全风险', key: 'risks', width: 40 },
{ header: '质量问题', key: 'quality', width: 40 },
{ header: '识别时间', key: 'time', width: 20 }
];
// 设置表头样式
const headerRow = worksheet.getRow(1);
headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } };
headerRow.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF4472C4' }
};
headerRow.height = 25;
// 分批处理记录(避免内存溢出)
const batchSize = 10;
for (let i = 0; i < records.length; i += batchSize) {
const batch = records.slice(i, i + batchSize);
for (const record of batch) {
const rowIndex = worksheet.rowCount + 1;
const row = worksheet.addRow({
id: i + 1,
stage: record.stage || '-',
riskLevel: this.getRiskLevelText(record.riskLevel),
risks: record.safetyRisks?.join('; ') || '无',
quality: record.qualityIssues?.join('; ') || '无',
time: new Date(record.createdAt).toLocaleString()
});
// 插入图片缩略图
if (record.imagePath && fs.existsSync(record.imagePath)) {
try {
const imageBuffer = fs.readFileSync(record.imagePath);
const imageId = workbook.addImage({
buffer: imageBuffer,
extension: 'jpeg'
});
worksheet.addImage(imageId, {
tl: { col: 1, row: rowIndex },
br: { col: 2, row: rowIndex },
editAs: 'oneCell'
});
row.height = 100; // 设置行高以容纳图片
} catch (err) {
console.error(插入图片失败: ${record.imagePath}, err);
}
}
// 交替行背景色
if (i % 2 === 0) {
row.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFF2F2F2' }
};
}
}
}
// 自动调整列宽
worksheet.columns.forEach(column => {
column.width = Math.max(column.width, 10);
});
// 流式写入文件
await workbook.xlsx.writeFile(outputPath);
console.log(Excel导出成功: ${outputPath});
}
getRiskLevelText(level) {
const map = { low: '低风险', medium: '中风险', high: '高风险' };
return map[level] || level;
}
}
module.exports = new ExportService();
3. PDF报告生成功能 (backend/src/services/pdfService.js)
javascript
const PDFDocument = require('pdfkit');
const fs = require('fs');
const path = require('path');
class PDFService {
constructor() {
// 注册中文字体(思源黑体)
this.fontPath = path.join(__dirname, '../fonts/SourceHanSansSC-Regular.otf');
}
/**
-
生成AI识别PDF报告
-
@param {Array} records - 识别记录
-
@param {string} outputPath - 输出路径
*/
async generatePDFReport(records, outputPath) {
return new Promise((resolve, reject) => {
const doc = new PDFDocument({
size: 'A4',
margin: 50,
font: this.fontPath
});const stream = fs.createWriteStream(outputPath);
doc.pipe(stream);// 封面
this.addCoverPage(doc, records.length);// 内容页
records.forEach((record, index) => {
if (index > 0) doc.addPage();
this.addRecordPage(doc, record, index + 1);
});// 页脚
this.addFooter(doc, records.length);doc.end();
stream.on('finish', () => {
console.log(PDF报告生成成功: ${outputPath});
resolve();
});stream.on('error', reject);
});
}
addCoverPage(doc, count) {
doc.fontSize(24).font(this.fontPath).text('AI智能识别分析报告', { align: 'center' });
doc.moveDown(2);
doc.fontSize(16).text(生成日期: ${new Date().toLocaleDateString()}, { align: 'center' });
doc.text(识别记录总数: ${count} 条, { align: 'center' });
doc.moveDown(4);
doc.fontSize(12).text('本报告由 AI 图像识别系统自动生成', { align: 'center' });
}
addRecordPage(doc, record, pageNum) {
doc.fontSize(18).font(this.fontPath).text(记录 #${pageNum}: ${record.stage || '未知阶段'}, { underline: true });
doc.moveDown(1);
// 添加图片
if (record.imagePath && fs.existsSync(record.imagePath)) {
try {
doc.image(record.imagePath, 50, doc.y, { width: 200 });
} catch (err) {
doc.text('[图片加载失败]', 50, doc.y);
}
}
doc.moveDown(1);
// 右侧文本信息
doc.fontSize(12).font(this.fontPath);
doc.text(风险等级: ${this.getRiskLevelText(record.riskLevel)}, 270, doc.y - 150);
doc.text(识别耗时: ${record.recognitionTime}ms, 270, doc.y + 20);
doc.text(安全问题: ${record.safetyRisks?.length || 0} 项, 270, doc.y + 40);
doc.moveDown(2);
doc.text('详细分析:', { underline: true });
doc.moveDown(0.5);
doc.fontSize(10);
doc.text(- 安全风险: ${record.safetyRisks?.join(', ') || '无'});
doc.text(- 质量问题: ${record.qualityIssues?.join(', ') || '无'});
doc.text(- 改进建议: ${record.suggestions?.join(', ') || '无'});
}
addFooter(doc, totalPages) {
const pages = doc.bufferedPageRange();
for (let i = 0; i < pages.count; i++) {
doc.switchToPage(i);
doc.fontSize(10).font(this.fontPath);
doc.text(第 ${i + 1} / ${pages.count} 页, 50, doc.page.height - 50, { align: 'center' });
}
}
getRiskLevelText(level) {
const map = { low: '低风险', medium: '中风险', high: '高风险' };
return map[level] || level;
}
}
module.exports = new PDFService();
4. 前端历史记录页面 (frontend/src/pages/AIRecognitionHistory.js 核心部分)
jsx
import React, { useState, useEffect } from 'react';
import { Table, Tag, DatePicker, Select, Button, Space, Statistic, Row, Col } from 'antd';
import { DownloadOutlined, EyeOutlined } from '@ant-design/icons';
import EChartsReact from 'echarts-for-react';
import dayjs from 'dayjs';
const AIRecognitionHistory = () => {
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(false);
const [filters, setFilters] = useState({
startDate: dayjs().subtract(7, 'day'),
endDate: dayjs(),
riskLevel: undefined,
success: undefined
});
// 获取历史记录
const fetchLogs = async () => {
setLoading(true);
try {
const params = {
startDate: filters.startDate.format('YYYY-MM-DD'),
endDate: filters.endDate.format('YYYY-MM-DD'),
riskLevel: filters.riskLevel,
success: filters.success
};
const response = await fetch('/api/ai/logs', {
method: 'GET',
params
});
const data = await response.json();
setLogs(data.data);
} catch (error) {
message.error('获取历史记录失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchLogs();
}, [filters]);
// 表格列定义
const columns = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '施工阶段', dataIndex: 'stage', width: 120 },
{
title: '风险等级',
dataIndex: 'riskLevel',
render: (level) => {
const colorMap = { low: 'green', medium: 'orange', high: 'red' };
return <Tag color={colorMap[level] || 'default'}>{level};
}
},
{ title: '安全问题数', dataIndex: 'safetyRisksCount', width: 100 },
{ title: '识别耗时(ms)', dataIndex: 'recognitionTime', width: 120 },
{
title: '状态',
dataIndex: 'success',
render: (success) => success ?
},
{
title: '操作',
render: (_, record) => (
<Button icon={
<Button icon={
)
}
];
// ECharts 统计图表配置
const chartOption = {
tooltip: { trigger: 'axis' },
legend: { data: ['成功率', '平均耗时'] },
xAxis: { type: 'category', data: logs.map(l => l.createdAt) },
yAxis: [
{ type: 'value', name: '成功率(%)' },
{ type: 'value', name: '耗时(ms)' }
],
series: [
{
name: '成功率',
type: 'line',
data: logs.map(l => l.success ? 100 : 0),
yAxisIndex: 0
},
{
name: '平均耗时',
type: 'bar',
data: logs.map(l => l.recognitionTime),
yAxisIndex: 1
}
]
};
return (
<div style={{ padding: '24px' }}>
📊 AI识别历史记录
{/* 筛选栏 */}
<Space style={{ marginBottom: 16 }}>
<DatePicker.RangePicker
value={[filters.startDate, filters.endDate]}
onChange={(dates) => setFilters({...filters, startDate: dates[0], endDate: dates[1]})}
/>
<Select placeholder="风险等级" allowClear onChange={(val) => setFilters({...filters, riskLevel: val})}>
<Select.Option value="low">低风险</Select.Option>
<Select.Option value="medium">中风险</Select.Option>
<Select.Option value="high">高风险</Select.Option>
<Button icon={
{/* 统计图表 /}
<Row gutter={16} style={{ marginBottom: 24 }}>
<EChartsReact option={chartOption} style={{ height: 300 }} />
<Statistic title="平均成功率" value={(logs.filter(l=>l.success).length/logs.length
<Statistic title="平均耗时" value={(logs.reduce((a,b)=>a+b.recognitionTime,0)/logs.length).toFixed(0)} suffix="ms" />
{/* 数据表格 */}
<Table
columns={columns}
dataSource={logs}
loading={loading}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
);
};
export default AIRecognitionHistory;

浙公网安备 33010602011771号