ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Node.js+Vue.js的检测报告下载网站实战

基于Node.js+Vue.js的检测报告下载网站实战 上个月给浙江艾艺塑业设计公司做的那个产品检测报告下载网站终于收尾交付了。整套系统基于 Node.js Vue.js 开发业务本身不复杂但做完之后我觉得很能代表制造型企业数字化转型里的一类典型场景——把散落在销售手里、微信聊天记录里、个人邮箱里的检测报告变成线上可查、可控、可追踪的自助下载资源。如果你是做企业信息化、外包开发或者自己公司刚好有类似“给客户发报告”的痛点这篇记录应该值得你花几分钟看完。先说清楚这个网站到底在解决什么问题。浙江艾艺塑业设计公司的主业是塑料制品、包装结构设计和制造客户里有食品包装企业、日用品品牌方也有一部分出口订单。这类业务有个绕不开的要求下游客户和采购方会索要产品的第三方检测报告比如食品接触材料检测、RoHS、REACH、拉伸强度测试等。报告来自不同检测机构格式不统一有效期也长短不一。以前全靠业务员微信找文件、邮箱发附件经常出现三种情况报告在某个同事电脑里人出差了拿不到给甲客户的报告被转手发给了乙完全没记录报告快过期了没人提醒等客户来要才发现版本已经失效。所以客户提的需求非常简单直白做一个网站让客户自己能登录、能看报告、能下载报告同时公司内部能管理上传和查看下载记录。围绕这个需求技术方案选型的时候没有太多犹豫前后端分离后端用 Node.js 的 Express前端用 Vue 3 Element Plus数据库用 MySQL文件存本地磁盘。选择 Node.js 不是因为追新而是这类以文件上传、下载、接口鉴权为主的中小型系统Node.js 的异步模型和生态处理起来非常顺手开发效率也高。Vue 生态里 Element Plus 的后台组件足够成熟表格、上传、表单这些核心场景基本不需要自己造轮子。下面我把整个项目的设计和落地过程按顺序拆开讲包括数据库表结构、关键接口、部署操作以及我实际踩过的几个坑。1. 项目背景与需求梳理1.1 检测报告管理的真实痛点做这个项目之前我专门花了两天时间去客户那边摸底跟着业务部的人看了他们平时怎么找报告、怎么发报告。真实情况比想象中更原始报告文件一部分在销售个人电脑里一部分在共享盘的嵌套文件夹里还有一部分甚至压缩包埋在邮件附件里。客户问过来要报告销售先问同事找不到就找检测机构重新要一份周期长体验差。更麻烦的是食品包装行业对检测报告的管理有合规要求哪些产品对应哪些报告、报告是否在有效期内、什么时间给哪个客户发过什么文件这些如果完全没有记录验厂审计的时候会非常被动。所以这个网站从一开始就不是简单的“文件下载器”它的核心价值是建立一份“报告资产台账”。每个产品对应哪些报告每份报告是什么版本、什么时候过期、下载过多少次、谁下载的全部要能查得到。下载体验要尽量简单但后台管理和审计能力必须扎实。1.2 功能需求清单确认需求调研结束后我整理了一份核心功能清单和客户过了两轮才定稿。这里分享出来基本覆盖了同类项目最常见的功能边界报告管理支持 PDF、图片等常见格式上传自动识别产品编号、报告类型、检测机构、报告有效期。分类与搜索按产品系列、检测类型、报告状态有效/即将过期/已过期筛选支持关键字搜索。客户权限客户登录后只能看到授权给自己的产品报告不能越权访问其他客户的数据。下载控制下载必须经过登录鉴权记录下载人、时间、IP、文件信息生成审计日志。版本管理同一产品的检测报告如果重新出具新版本上传后旧版本仍可追溯保留历史记录。过期提醒后台首页显示即将到期的报告列表必要时通过站内提示或后续扩展短信、邮件通知。后台管理管理员上传报告、维护产品资料、管理客户账号、查看下载统计。确认需求时还有一个容易漏掉的细节报告的有效期不是简单从“出报告日期”算一年很多检测标准有明确的生效日期和到期日期甚至有的报告会标注“样品仅对来样负责”这类说明性文字不能丢。所以我们额外加了一个“报告备注”字段专门存放检测机构给出的限定说明避免客户下载后误读报告适用范围。1.3 为什么选 Node.js Vue.js 这个组合很多朋友会问这类系统用 Java 或者 PHP 做不也一样吗确实一样能实现但结合项目实际情况Node.js Vue.js 是更合适的答案。第一团队技术栈偏前端使用同一种语言开发前后端沟通成本和维护成本最低第二系统核心场景是大量小文件下载和并发请求Node.js 的异步非阻塞模型在这种 I/O 密集型场景下表现很理想不需要像传统 Java 应用那样启动一个重型容器第三部署运维简单一个 Node 进程加 Nginx 转发就能跑起来客户的服务器是 CentOS 7.9 的 2 核 4G 机器资源占用非常友好。前端选择 Vue 3 Element Plus 而不是 React主要考虑是 Element Plus 的表格、上传、表单校验、分页组件能直接覆盖后台管理 90% 的界面需求。开发进度上有明显优势组件风格统一客户看了也说“这个后台像正经管理系统”。如果追求更炫酷的交互或者需要更复杂的图表再考虑引入其他库但核心场景里它足够可靠。2. 核心模块与数据设计2.1 文件存储与访问路径设计检测报告本质上是文件但文件怎么存、路径怎么设计直接决定系统后续好不好维护。这次项目我选了最简单的本地磁盘存储方案在服务器上单独划分一个 /data/reports 目录内部按照“产品编号/报告类型/文件版本”的层级存放文件。例如 /data/reports/P1001/REACH/F001.pdf。这样即使数据库发生意外纯靠目录结构也能大概定位到文件。文件存储有个关键的工程决策数据库里不存文件的绝对路径只存相对路径如 P1001/REACH/F001.pdf同时把上传时原始文件名单独存储。这样系统迁移、换存储位置、甚至接入 OSS 对象存储时只需要修改一个基础配置不需要动数据库已经存在的记录。下载时的文件名用数据库里的原始文件名拼接避免用户下载到一长串无意义编号。这里提一个我踩过的坑最初我图省事直接把文件原名称作为存储文件名结果两个业务员上传了同名文件后上传的覆盖了先上传的报告文件丢失。后来改成统一按“报告编号_版本号_上传时间戳.pdf”生成存储文件名彻底解决重名问题。用户实际下载看到的是原始文件名不影响体验。2.2 用户权限与鉴权流程设计企业报告下载网站不能像公共资源站那样全开放客户只能看到和自己业务相关的报告。权限模型的中心是“产品授权”即客户和产品是多对多关系。管理员在后台维护产品和客户然后勾选授权关系。客户登录后系统根据授权关系返回产品列表报告查询和下载都限定在这个范围内。登录鉴权采用 JWT理由是无状态、扩展性好不需要在服务端维护 Session适合这种前后端分离且可能后续接入小程序、App 的场景。JWT payload 里存用户 ID、角色和授权版本号。这里有个细节如果后台中途修改了客户的授权范围已经签发的 token 里没有新权限信息那么必须有一个机制让权限变更尽快生效。我在用户表增加了一个 auth_version 字段每次修改授权就加 1登录签发 token 时把 auth_version 放进去后端鉴权中间件每次请求都校验当前用户的 auth_version 和 token 里的是否一致不一致就要求重新登录。牺牲一点点体验换来权限收放的即时性我认为值得。2.3 数据库表结构设计这是整套系统的地基。表设计上我保留了扩展余地没有做过度设计。核心表包括用户表、客户表、产品表、报告表、报告版本表、客户产品授权表、下载日志表。为了节省篇幅这里只列出最核心的两张表。CREATE TABLE reports ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL COMMENT 关联产品ID, report_code VARCHAR(50) NOT NULL COMMENT 报告编号, report_type VARCHAR(30) NOT NULL COMMENT 检测类型REACH/RoHS/食品接触等, origin_filename VARCHAR(255) NOT NULL COMMENT 原始文件名用于下载展示, storage_path VARCHAR(255) NOT NULL COMMENT 存储相对路径, file_size BIGINT NOT NULL COMMENT 文件字节数, issued_date DATE NULL COMMENT 出报告日期, expire_date DATE NULL COMMENT 报告到期日期, note TEXT NULL COMMENT 报告备注检测机构限定说明等, is_latest TINYINT DEFAULT 1 COMMENT 是否当前有效版本, version INT DEFAULT 1 COMMENT 版本号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_product (product_id), KEY idx_expire (expire_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;CREATE TABLE download_logs ( id INT PRIMARY KEY AUTO_INCREMENT, report_id INT NOT NULL, user_id INT NOT NULL, customer_id INT NULL COMMENT 客户ID便于统计, ip VARCHAR(64) NULL, user_agent VARCHAR(255) NULL, download_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_report (report_id), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;报告版本表的逻辑是每次上传同一产品同一类型的新报告旧记录 is_latest 置为 0新记录版本号加 1。这样报告更新后后台列表默认展示最新版本但打开“历史版本”还能找到旧文件。审计客户要的就是这种可追溯能力。2.4 核心接口清单后端接口遵循 RESTful 风格核心接口列表如下接口方法作用权限/api/auth/loginPOST用户登录签发 JWT公开/api/productsGET获取当前用户有权限的产品列表登录用户/api/reportsGET按产品、类型、状态查询报告登录用户/api/reports/{id}/downloadGET下载报告文件写日志登录用户/api/admin/reportsPOST上传新报告管理员/api/admin/reports/{id}PUT修改报告信息管理员/api/admin/reports/{id}DELETE逻辑删除报告管理员/api/admin/download-logsGET查询下载日志和统计管理员下载接口不是直接暴露静态文件链接而是通过后端接口转发。这样做的原因有三可以加权限校验可以动态拼接存储路径可以顺手写一条下载日志。后面我会给出这个接口的完整实现代码。3. 从零到上线实操过程记录3.1 环境准备Node.js 版本选择和安装项目运行环境是 CentOS 7.9我选择了 Node.js 18.20.4 LTS 版本。可能有人会问Node.js 22 都已经发布了为什么不用新版本我的原则是生产环境求稳18 LTS 还处于维护期生态兼容性最好很多老项目依赖的第三方包在 22 下可能没跟上。如果是从零开始且不依赖老旧包也可以选 22但没必要在这个项目里冒险。CentOS 7.9 下安装 Node.js 18.20.4 的步骤非常简单用官方编译好的二进制包解压即可。完整命令记录如下# 下载 Node.js 18.20.4 LTS 官方 Linux x64 二进制包 wget https://nodejs.org/dist/v18.20.4/node-v18.20.4-linux-x64.tar.xz # 解压到 /usr/local/ 目录 tar -xJf node-v18.20.4-linux-x64.tar.xz -C /usr/local/ # 重命名目录方便后续操作 mv /usr/local/node-v18.20.4-linux-x64 /usr/local/nodejs # 配置环境变量 echo export PATH/usr/local/nodejs/bin:$PATH /etc/profile.d/nodejs.sh source /etc/profile.d/nodejs.sh # 验证安装 node -v npm -v如果服务器网络下载慢也可以先把安装包下载到本地再上传。npm 默认源在国内经常超时我会第一时间设置镜像源提高依赖安装速度npm config set registry https://registry.npmmirror.com这个环境准备过程同样适用于本地开发机Windows 和 macOS 下无非是安装包格式不同Node 版本管理我推荐用 nvm方便切换不同项目需要的版本。3.2 后端服务搭建与关键代码实现后端我使用 Express 框架项目结构按功能模块拆分routes、controllers、services、middlewares、config。文件上传用 multer数据库连接用 mysql2 连接池。为方便维护启动脚本和路由配置放在入口文件里统一管理。下面是后端入口文件和中间件配置的核心代码省略了部分业务细节但保留了关键逻辑const express require(express); const cors require(cors); const path require(path); const authMiddleware require(./middlewares/auth); const reportRoutes require(./routes/reports); const adminRoutes require(./routes/admin); const app express(); app.use(cors({ origin: [https://yourdomain.com], credentials: true })); app.use(express.json()); // 健康检查 app.get(/api/health, (req, res) res.json({ ok: true })); // 业务路由 app.use(/api/auth, require(./routes/auth)); app.use(/api, authMiddleware.verifyUser, reportRoutes); app.use(/api/admin, authMiddleware.verifyAdmin, adminRoutes); // 统一异常处理 app.use((err, req, res, next) { console.error(err); res.status(err.status || 500).json({ message: err.message || 服务器错误 }); }); app.listen(3000, () { console.log(report server running at http://localhost:3000); });上传接口用 multer 处理限制文件类型为 PDF 和图片单个文件最大 50MB。注意 multer 的 diskStorage 不能直接决定最终存储路径我是在文件存到临时目录后再通过业务逻辑移动到按产品编号生成的目录中。这样可以让目录结构更可控避免多个业务接口同时上传同一个产品文件时出现文件名冲突。const multer require(multer); const upload multer({ storage: multer.diskStorage({ destination: (req, file, cb) cb(null, /tmp/upload_cache), filename: (req, file, cb) { const ext path.extname(file.originalname); cb(null, temp_${Date.now()}_${Math.random().toString(36).slice(2)}${ext}); } }), limits: { fileSize: 50 * 1024 * 1024 }, fileFilter: (req, file, cb) { if ([application/pdf, image/jpeg, image/png].includes(file.mimetype)) { cb(null, true); } else { cb(new Error(仅支持 PDF、JPG、PNG 文件)); } } });下载接口是整个网站的核心代码里做了三件事根据报告 ID 查询是否存在且是当前有效版本检查当前用户对该产品是否有授权执行文件下载并写日志。实际返回文件时用 res.downloadNode.js 会自动处理内容类型和附件下载行为。中文文件名乱码问题在常见问题那节详细说。router.get(/reports/:id/download, async (req, res, next) { try { const reportId Number(req.params.id); const [rows] await db.query( SELECT r.*, p.customer_id FROM reports r JOIN customer_products p ON r.product_id p.product_id WHERE r.id ? AND r.is_latest 1, [reportId] ); if (!rows.length) { return res.status(404).json({ message: 报告不存在或已失效 }); } // 校验当前用户是否属于报告对应的客户 if (req.user.role ! admin rows[0].customer_id ! req.user.customerId) { return res.status(403).json({ message: 无权下载该报告 }); } const absPath path.join(REPORT_BASE_DIR, rows[0].storage_path); // 写日志 await db.query( INSERT INTO download_logs (report_id, user_id, customer_id, ip, user_agent) VALUES (?, ?, ?, ?, ?), [reportId, req.user.id, req.user.customerId, req.ip, req.headers[user-agent] || ] ); res.download(absPath, rows[0].origin_filename); } catch (err) { next(err); } });这里需要注意一个安全细节不能直接用 req.params.id 拼进 SQL必须参数化查询。同时下载文件前一定要用 path.resolve 把绝对路径确认一遍防止通过路径穿越读取服务器上的其他文件。我在实际项目里加过一层校验const absPath path.resolve(path.join(REPORT_BASE_DIR, rows[0].storage_path)); if (!absPath.startsWith(path.resolve(REPORT_BASE_DIR))) { return res.status(403).json({ message: 非法文件路径 }); }3.3 前端页面实现Vue 3 Element Plus前端工程用 Vite 创建初始化命令是 npm create vitelatest report-web -- --template vue。管理后台的界面结构是三块顶部导航栏、左侧菜单、右侧内容区。客户端的下载页面更简单一个产品筛选区加一个报告表格。前端核心组件是报告列表页我用 Element Plus 的 el-table 展示报告el-upload 实现管理端上传。表格列包括报告类型、产品编号、报告有效期、文件大小、状态标签和操作按钮。状态标签根据当前日期动态计算有效期还剩 30 天以内的标为“即将过期”已过期的标为“已过期”用 el-tag 的不同 type 区分。下载按钮的处理比较容易被忽略点击后先用 axios 发请求获取 blob再通过 URL.createObjectURL 创建本地下载链接。为什么不用 el-link 直接指向后端接口因为在生产环境容易出现权限校验不严格、无法携带 token 的问题。用 axios 请求可以方便地在拦截器里统一添加请求头响应拿到 blob 之后手动触发浏览器下载。import axios from axios; import { ElMessage } from element-plus; const api axios.create({ baseURL: /api, timeout: 30000 }); api.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); async function downloadReport(row) { try { const res await api.get(/reports/${row.id}/download, { responseType: blob }); const blobUrl URL.createObjectURL(new Blob([res.data])); const link document.createElement(a); link.href blobUrl; link.download row.origin_filename; document.body.appendChild(link); link.click(); URL.revokeObjectURL(blobUrl); link.remove(); } catch (e) { ElMessage.error(e.response?.data?.message || 下载失败); } }这里还有一个容易踩的坑如果响应不是正常的文件流而是后端返回的 JSON 错误信息axios 返回的也是 blob 类型直接下载会得到一个写着“无权下载”内容的 txt 文件。所以我在拦截器响应里对 blob 类型做了二次处理如果是 JSON就用 FileReader 解析出来并提示用户而不是把错误当文件下载。3.4 前后端联调与跨域处理开发环境下前端跑在 Vite 的 5173 端口后端跑在 3000 端口必然有跨域问题。我的处理方式是在 Vite 配置文件里设置开发代理让前端把 /api 开头的请求都转发到后端前端代码里请求路径统一写成相对路径 /api这样生产环境和开发环境的请求路径完全一致。// vite.config.js export default { server: { port: 5173, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } };生产环境就更简单了前端打包成静态文件交给 Nginx 直接托管Nginx 再把 /api 请求转发到 Node 服务。前后端最终在同一个域名下都是同源请求CORS 只要在开发环境配合即可。这里不配置跨域开放对安全性也有好处别人的网站拿不到我们的接口数据。补充一句我见过不少项目联调时被跨域问题折腾半天其实绝大多数情况是配置了前端代理却忘了在 axios 里用相对路径或者后端 CORS 和前端代理同时打开导致请求经过两次“处理”出现怪异的预检错误。开发代理和后端 CORS 二选一即可不用同时配置。3.5 生产部署PM2 管理进程 Nginx 转发生产环境部署方案很常规Node 服务用 PM2 守护前端静态文件由 Nginx 托管/api 路径的请求全部交给 Node 服务处理。PM2 安装和启动命令如下npm install -g pm2 # 进入项目目录后启动 pm2 start app.js --name report-server # 设置开机自启动 pm2 startup pm2 save # 查看日志 pm2 logs report-serverNginx 配置是整个部署过程的重头戏。我的配置里做了三件重要的事把前端静态文件根目录指到 dist 文件夹把 /api 请求转发给本机 3000 端口的 Node 服务设置了上传文件大小上限为 50MB因为报告文件里有很多扫描件和 PDF动不动就十几 MBNginx 默认的 1MB 上传限制会直接报 413。server { listen 80; server_name yourdomain.com; client_max_body_size 50m; root /var/www/report-web/dist; index index.html; # 前端 Vue 路由 history 模式支持 location / { try_files $uri $uri/ /index.html; } # API 反向转发到 Node.js 服务 location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ { expires 30d; add_header Cache-Control public, no-transform; } }注意 location /api/ 和 proxy_pass http://127.0.0.1:3000 的组合结尾不要加斜杠这样 /api/reports 会被完整转发到后端的 /api/reports而不会丢失前缀。如果后端路由不带 /api 前缀则需要把 location 写成 /api/ 并把 proxy_pass 写成 http://127.0.0.1:3000/ 去掉前缀。这两种方式是新手最容易搞混的地方实际部署时先 curl 一下接口确认路径正确再继续。部署完成后我一般还会用 curl 做一轮接口冒烟测试确认登录、报告列表、下载三个核心链路都通了才交给客户验收。4. 真实踩坑常见问题与排查方法4.1 中文文件名下载乱码这个问题几乎每个做文件下载的人都会遇到。Node.js 的 res.download 在设置 Content-Disposition 响应头时如果文件名包含中文浏览器默认按 ASCII 解析会显示成乱码。正确做法是同时提供 filename 和 filename* 两种格式filename 用原始值兜底filename* 采用 UTF-8 百分号编码。我封装了一个工具函数专门处理下载文件名function setDownloadFileName(res, fileName) { const encoded encodeURIComponent(fileName); res.setHeader(Content-Disposition, attachment; filename${fileName}; filename*UTF-8${encoded}); }实测下来这个写法兼容 Chrome、Firefox、Safari 和 Edge。之前我只写 filename*老版本浏览器会直接下载一个名为“UTF-8”的文件加上 filename 兜底之后问题消失。4.2 上传大文件报 413 或超时客户上传的报告里有不少是扫描仪输出的高分辨率 PDF单个文件二三十 MB 很常见。部署上去之后第一天就有人反馈“上传失败”后台一看 Nginx 返回 413 Request Entity Too Large。原因就是 Nginx 默认 client_max_body_size 只有 1MB我在前面 Nginx 配置里加了一行 client_max_body_size 50m问题解决。如果你用的是对象存储或者服务器有额外的网关同样需要检查网关层有没有上传大小限制。另外 Node 服务这边multer 的 limits 也要和 Nginx 保持一致。我曾经遇到 Nginx 都放行了但 multer 这边限制了 10MB上传大文件直接报 MulterError两边配置不一致是这种问题最常见的根源。还有一个隐蔽点服务器前面的网关超时时间如果太短上传大文件过程中连接被断开也会导致上传失败排查时要看 Nginx 的 error.log 和 Node 应用的 stdout 日志。4.3 客户下载了别人的报告这个问题的严重级别最高也是在权限测试阶段发现的。最初的下载接口只校验用户是否登录没校验他对产品是否有权限结果前端通过修改报告 ID 参数就能下载其他客户的产品报告。好在这是内部测试时发现的没有造成实际数据泄漏。修复的方式就是前面下载接口代码里展示的那一层校验根据报告关联的产品反查当前登录用户的客户授权关系没有关联直接返回 403。这里要强调永远不要相信前端传过来的任何“权限信息”比如前端把可以下载的报告 ID 列表发给后端那是完全错误的做法。后端每次下载都必须重新从数据库查询权限关系因为报告和客户的授权关系随时可能被后台修改。权限这块还需要注意报表列表接口和下载接口要保持相同的权限过滤逻辑最好抽成同一个 service 层方法调用。我之前在两个接口里分别写了两遍查询逻辑列表查出来了 A 客户的产品下载接口却因为 join 条件不同查出来 B 客户的报告漏了一个关联条件后面对比排查了很久。4.4 前端打包后接口 404Vue 路由使用 history 模式时刷新页面容易出现 404因为 Nginx 找不到对应路由的静态文件。解决办法是配置 try_files $uri $uri/ /index.html让所有非文件请求都回退到 index.html。这个配置在 Nginx 配置示例里已经有了。另一个 404 前后端都容易踩后端接口路径大小写不一致。Express 默认区分大小写前端请求写 /api/Products后端路由定义是 /api/products就会 404。这个没有特别好的办法建议前端统一用小写后端路由中间件加一个统一小写转换的中间件或者干脆用 NestJS 这类自带路由规范的框架。经验之谈接口路径从第一个版本就固定好上线之后不要随意改改一次就是一次线上事故。4.5 下载日志统计不准确下载统计是客户很关注的一个功能他们想通过报表看出哪个产品最常被客户下载。最初统计时直接 count(download_logs)但发现一个问题客户可能在下载时网络中断重试了几次同一个报告同一个用户短时间内会产生多条记录导致统计数字虚高。我的解决方案不是去重日志而是保留所有原始日志统计时按照“用户 ID 报告 ID 五分钟内”做一次合并去重形成一个专门的报表查询视图。这样的好处是如果需要审计原始行为日志还在如果只是看业务热度可以用去重后的统计。数据库查询里用 GROUP BY 加时间窗口判断性能在数据量不夸张的情况下完全够用。4.6 部署环境路径分隔符导致文件找不到开发机是 Windows服务器是 Linux上传文件时如果程序里拼接路径用了反斜杠部署到 Linux 上就会找不到文件。这个问题很经典解决方式很简单所有路径拼接一律用 path.join不要手写字符串拼接路径。特别注意不要在目录路径末尾手写 / 或者 \由 path.join 自己去适配操作系统。另外数据库里存储的相对路径统一用正斜杠 / 分隔即使 Windows 上 path.join 会产生反斜杠写入数据库之前也做一次 replace(/\/g, /) 的转换。这样保证数据库里的路径格式与具体操作系统无关后续迁移存储也不受影响。5. 运维经验与后续扩展方向5.1 报告过期提醒的实现思路报告过期是行业合规里绕不开的痛点项目里我在后台首页加了一个“过期预警”模块查询所有 is_latest 1 且 expire_date 小于当前日期加 30 天的报告按剩余天数排序展示。产品经理看到这个列表就知道哪些报告需要联系检测机构重新送检。如果后续要升级为主动提醒可以在 Node 服务里引入 node-schedule每天凌晨扫描一次过期报表然后把提醒消息推到钉钉群或企业微信。这里有一个细节要提前设计好报告过期提醒不仅看 expire_date还要结合报告状态字段因为有些报告虽然日期过期了但检测机构出具了延期声明这类报告需要在系统里单独标记为“已延期”避免天天告警误报。5.2 文件备份与安全加固报告文件是客户的重要资产我特别强调过服务器一定要做磁盘快照和异地备份。当前方案是每天凌晨通过 rsync 把 /data/reports 同步到另外一台内网机器数据库用 mysqldump 定时备份备份保留 30 天。如果有条件更推荐把文件同步到对象存储并开启生命周期管理成本很低安全性和可用性都高很多。安全加固方面做三件事第一上传目录不对外暴露所有文件都通过带权限校验的接口下载防止绕过系统直接访问文件第二上传渠道严格校验文件类型和大小防止恶意上传可执行文件或其他危险内容第三给后台管理接口单独增加访问 IP 白名单只允许公司内部出口 IP 访问管理页面客户下载页面不受限制。登录页还加了连续失败锁定策略同一个账号连续输错五次密码锁定 15 分钟。5.3 这个项目还能怎么扩展交付之后我还给客户提了几个扩展方向都是基于现有结构可以平滑升级的。第一下载页面增加“保密协议确认弹窗”客户下载某些敏感报告前需要勾选“仅用于审核评估不擅自传播”满足更高要求的合规场景。第二报告详情页嵌入在线预览用 pdf.js 直接在浏览器里查看 PDF减少“下载后发现不是自己要的文件”的无效操作。第三对接企业微信通知上传新报告后自动发消息给关联客户比客户主动来问“报告出了吗”体验好得多。更进一步如果企业希望客户查报告更方便可以做一个微信公众号 H5 页面复用现有 JWT 鉴权机制客户微信扫码后自动登录报告直接在线查、在线下。这个扩展在架构上不需要改动后端接口加一个移动端前端工程就能实现后端接口复用度很高。说回这个项目本身我个人在实际操作中最深的一点体会是这类内部系统表面上拼的是技术实现实际上拼的是数据规范。代码里的坑都可以通过调试解决但报告文件的命名规则、产品编号的标准化、客户授权的维护流程这些没有技术含量却决定系统是否好用的东西往往最容易被忽略。如果你也要做类似的报告下载网站我建议开工第一天先和客户定死“产品编号规范”和“报告文件命名规范”等数据积累几千份之后再想回头整理成本高到你想哭。另外最后分享一个对这类项目很管用的小技巧上线第一周不要只盯着系统日志要多看客户实际会怎么操作。我们上线后第三天就发现不少客户习惯用手机打开网站原来的下载按钮在手机浏览器里弹不出下载行为赶紧加了一个移动端兼容处理才保证了使用体验。做企业应用的都知道客户不会告诉你他们想要什么但他们会通过点击行为直接告诉你哪里不好用。多观察真实使用场景比多写一千行代码更有价值。
返回列表