ARTICLE DETAIL

资讯详情

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

基于Node.js+Vue的二手交易平台全栈开发实战解析

基于Node.js+Vue的二手交易平台全栈开发实战解析 之前带过几个学生做毕业设计其中不下三个人选的题目是“基于Node.js和Vue的二手交易平台”细分下来又有“ThinkPHP商家端”“SpringBoot后台”等各种变体。说实话第一次看到这种混合技术栈我也愣了一下用户端用Node.js做后端商家端又上ThinkPHP这不折腾吗但真正带着他们把整个系统从零到一搭完我才意识到这套组合背后其实是有现实考量的——一方面论文/答辩需要体现技术覆盖面另一方面前后端分离加双后端服务恰好能把权限边界、业务边界分开非常适合作为系统设计的展示案例。这篇文章就围绕“二手交易平台的系统设计与实现”展开聊聊我从需求拆解、数据库设计、Vue前端、Node.js后端到ThinkPHP商家端的完整落地思路最后再集中整理几个新手十有八九会遇到的环境配置坑尤其是那个让人抓狂的npm.ps1执行策略报错。内容会偏实操适合正在做类似课设、毕设或者想自学全栈的朋友直接参照。1. 需求拆解与整体系统设计1.1 先别写代码把二手交易的核心链路捋清楚很多人拿到题目就急着搭Vue脚手架结果写到一半发现业务逻辑理不清。我建议第一步永远是画业务链路。二手交易平台表面上就是个“发布商品-浏览-下单-支付-收货-评价”的电商闭环但和全新商品电商有个显著区别交易主体不再是平台对买家而是商家对买家平台更多承担信息撮合与信用背书角色。把角色拆开看就清楚多了普通用户买家注册登录、浏览商品、搜索筛选、收藏、下单、支付通常是模拟支付、确认收货、评价、个人中心管理。商家卖家入驻申请、商品发布/编辑/下架、订单处理发货/退款、销售统计、提现或余额管理。平台管理员商家入驻审核、商品审核、用户管理、订单监管、分类管理、数据统计。这三类角色对应三种不同的前端入口和权限等级。所以在系统设计阶段我建议直接把“用户端”和“商家端/管理端”分开设计而不是硬揉在一个后台里。这也是标题里为什么会出现“ThinkPHP商家”的底层原因——商家端作为独立后台完全可以由独立后端服务支撑。1.2 这套技术栈的选型逻辑是折腾还是合理分工先说结论用Vue Node.js做用户端ThinkPHP做商家端从工程角度是合理的不是硬凑技术栈。原因有三点前后端分离是大势所趋。用户端面向C端流量页面交互多、异步请求频繁用Vue这类渐进式框架可以做到组件化开发、路由切换流畅。后端只写API不关心页面渲染职责单一。Node.js和ThinkPHP可以各司其职。Node.jsExpress非常适合做轻量API网关和实时消息推送比如站内信、订单状态变更通知而ThinkPHP作为老牌PHP框架在快速搭建管理后台接口、处理文件上传、对接MySQL方面非常顺手尤其适合商家端这类CRUD密集型的业务。从展示角度技术覆盖面广。一套系统里同时出现Vue、Node.js、ThinkPHP很容易在系统架构图里体现出“前后端分离 多服务协作”的设计理念答辩时也有的聊。当然如果你要杠“一台服务器凭什么跑两套后端”我只能说工程上确实可以合并但为了模块解耦和演示效果双后端配合Nginx反向代理区分域名路径比如 /api 走Node /admin_api 走ThinkPHP完全可行我后面部署部分会细说。1.3 功能模块的划分别让代码和权限纠缠在一起我常用的做法是按“角色 核心业务”双维度划分功能模块端角色核心功能模块用户端 Web买家/游客注册登录、商品浏览搜索、商品详情、购物车、下单、支付、订单管理、收藏、评价、站内信商家端 Web商家入驻申请、商品管理发布/编辑/上下架、订单处理、发货、退款处理、销售统计、余额/提现管理后台管理员商家审核、商品审核、分类管理、用户管理、订单全流程监管、数据概览功能模块定下来之后数据库表结构基本就能推导出来了。这也是我坚持“先理业务再谈技术”的原因——表设计的好坏直接决定你后面写接口是舒服还是想砸键盘。2. 数据库设计与核心表结构2.1 设计表结构时的三个关键决策二手交易平台表的数量不算多一般在10~15张左右但有几个设计决策很影响后续开发我踩过坑之后总结出来第一用户表和商家表必须分开。我第一版偷懒用了user表加role字段区分买家、商家、管理员结果发现商家需要存储店铺名称、营业执照、入驻审核状态、店铺公告等一堆字段塞在用户表里要么大量冗余字段要么逻辑混乱。后来老老实实拆成user表和merchant表两张表用user_id关联清爽多了。买家是纯C端用户商家是入驻主体两者字段差异太大强行合一等于自掘坟墓。第二所有业务表都要有status字段但含义要细分。二手交易平台最核心的“二手”特性在于商品状态。一件商品在平台上要流经待审核 - 上架中 - 已售出 - 下架/删除。订单要流经待付款 - 待发货 - 待收货 - 已完成 - 已取消/退款中。每个状态的枚举值必须在开发前定义好并写进接口文档否则前后端对同一个数字理解不一致联调时就是灾难现场。第三商品图片表单独建不要用逗号分割存JSON。我第一次图省事把多张图片存成字符串比如“/upload/1.jpg,/upload/2.jpg”写接口时确实爽但后来要做主图轮播、缩略图、图片懒加载才发现拆字符串是件多蠢的事。建一张goods_image表每张图一条记录按sort排序主图标1扩展性和灵活性完全不同。2.2 几张核心表的字段设计参考这里给出我的建表核心字段你可以直接参考user 用户表字段类型说明idint主键自增usernamevarchar(50)用户名唯一passwordvarchar(255)bcrypt加密后的密码avatarvarchar(255)头像地址phonevarchar(20)手机号balancedecimal(10,2)账户余额模拟支付用statustinyint1正常 0禁用created_atdatetime注册时间merchant 商家表字段类型说明idint主键user_idint关联用户表shop_namevarchar(100)店铺名称shop_logovarchar(255)店铺logodescriptiontext店铺简介audit_statustinyint0待审核 1通过 2拒绝audit_remarkvarchar(255)审核备注commission_ratedecimal(5,2)平台抽成比例如5.00表示5%goods 商品表字段类型说明idint主键merchant_idint商家IDtitlevarchar(100)商品标题descriptiontext商品描述category_idint分类IDpricedecimal(10,2)售价original_pricedecimal(10,2)原价/购买价qualitytinyint成色1全新 2 99新 3 95新 4 9成新 5 8成新及以下cover_imagevarchar(255)封面图stockint库存salesint销量statustinyint0待审核 1在售 2已下架 3已售罄is_deletetinyint软删除标记orders 订单表字段类型说明idint主键order_novarchar(32)唯一订单号user_idint买家IDmerchant_idint商家IDgoods_idint商品IDgoods_snapshottext商品快照下单时的标题/价格/图片amountdecimal(10,2)订单金额statustinyint订单状态机pay_timedatetime支付时间delivery_timedatetime发货时间finish_timedatetime完成时间address_infotext收货地址快照这里特别说明一下goods_snapshot字段。二手商品本来就是非标准品商家可能随时修改描述买家下单后必须有快照不然订单详情里显示的商品信息和现在页面上对不上容易起纠纷。任何电商系统订单相关表里存快照字段几乎是必须的操作。2.3 订单号生成与防重复提交设计订单号我推荐用时间戳 随机数 用户ID组合date(YmdHis) . str_pad(mt_rand(1, 9999), 4, 0, STR_PAD_LEFT) . substr($userId, -3)。虽然并发下极小概率会撞但配一个order_no唯一索引插入时冲突就重试一次完全够用。防重复提交是另一个容易被忽略的坑。用户在商品详情页疯狂点击“立即购买”很可能一次订单被插入好几条。两个手段配合前端点击后按钮置灰等接口返回再恢复从源头减少重复点击。后端在创建订单接口里做幂等校验同一用户对同一商品在同一个短时间窗口内比如2分钟如果已存在待付款订单直接返回该订单号不重复创建。这两条做到基本能避免百分之九十的重复订单问题。3. 前端Vue实战路由权限与接口封装3.1 用Vite搭一个Vue3项目目录结构这样分Vue3 Vite已经是目前新建项目的主流组合比Vue2 webpack的启动速度快太多。我建议直接上Vue3配合Composition API写业务代码组织清晰度比Options API高一个档次。初始化命令很简单npm create vitelatest second-hand-front进入项目后选择Vue JavaScript模板如果你熟悉TypeScript也可以选TS但毕设/课设用JS能省不少精力。我的目录结构一般这样定src/ api/ // 按业务模块拆分的接口请求 assets/ // 静态资源 components/ // 公共组件商品卡片、分页、图片懒加载等 router/ // 路由配置 路由守卫 stores/ // Pinia状态管理 views/ home/ // 首页 goods/ // 商品详情 order/ // 订单流程 user/ // 个人中心 merchant/ // 商家端视图 utils/ // 工具函数日期格式化、价格显示等把api单独拎出来是很多新手会忽略的重点。不要在每个Vue组件里直接写axios请求否则后端接口一变你得到处找代码改。统一放api/goods.js、api/order.js这种模块文件里每个函数导出组件里只负责调用维护成本直线下降。3.2 动态路由与路由守卫把权限做成闭环二手交易平台至少有三类角色如果你只写死一套静态路由所有用户都能访问商家后台页面那等于没做权限控制。我这里的做法是基础路由首页、商品详情、登录注册所有人可见权限相关路由购物车、订单、商家后台用动态路由按角色注入。路由守卫是拦路的第一道关卡// router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(token) const userInfo JSON.parse(localStorage.getItem(userInfo) || {}) // 需要登录才能访问的页面 if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) return } // 商家端页面除了登录还需要角色是商家或管理员 if (to.meta.requiresMerchant userInfo.role ! merchant userInfo.role ! admin) { next({ path: /403 }) return } next() })动态路由更细的做法是根据后端返回的角色权限在登录成功后用router.addRoute()动态注册商家端路由而不是一次性注册全部路由。这样从路由表层面普通用户根本不存在商家后台的路由记录安全性更强。我实际项目中两种方式都试过第一种简单直观适合课设第二种更贴近真实商用系统面试时能讲出亮点。3.3 axios拦截器里统一处理token和错误码接口请求封装是我每次带项目都强调的重点。axios实例建议单独建一个文件把baseURL、超时时间、请求拦截、响应拦截全部集中处理// utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) // 请求拦截每次请求自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) // 响应拦截统一处理错误码 request.interceptors.response.use( response { const res response.data // 约定后端返回格式 { code: 200, data: ..., msg: success } if (res.code 200) { return res.data } if (res.code 401) { // token过期清除登录态并跳转登录页 localStorage.removeItem(token) localStorage.removeItem(userInfo) router.push(/login) ElMessage.error(登录已过期请重新登录) return Promise.reject(new Error(unauthorized)) } ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) }, error { ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request这里有个小经验baseURL一定要用环境变量控制不要写死。我见过太多人写死http://localhost:3000/api换台机器跑就傻眼。Vite里在项目根目录建.env.development和.env.production分别写不同地址用import.meta.env.VITE_API_BASE_URL读取部署时不用改代码。另一个经验是token存储。用localStorage而不是sessionStorage。sessionStorage关掉浏览器就没了用户第二次打开又得登录体验很差。localStorage持久化配合路由守卫和401拦截已经能满足绝大多数场景。3.4 商品列表和详情页的可复用组件设计商品卡片是二手平台里复用频率极高的组件首页、搜索结果、商家店铺页、猜你喜欢都用到。我建议抽一个GoodsCard.vue组件props接收商品对象内部展示封面图、标题、价格、成色标签。列表页用el-pagination做分页从父组件传入总条数和当前页触底翻页或者点击翻页都可以。商品详情页稍微复杂一点涉及图片轮播、参数展示、商家信息卡片、同类推荐四个区块。图片轮播我直接用element-plus的el-carousel省心。商家信息卡片里放店铺名、信誉分、联系电话旁边放“进入店铺”按钮跳转商家店铺页。这里有一个运营层面的细节二手商品的详情页一定要突出成色和瑕疵描述因为二手商品的核心风险就在“图文不符”前端页面上哪怕多展示两张细节图都能大幅降低退货纠纷率。开发时可以在商品描述区支持图片直接嵌入富文本这样商家发布商品时能写得更直观。4. Node.js后端API设计与核心逻辑4.1 Express项目结构别把所有代码堆在index.jsNode.js后端我习惯用Express轻量、生态成熟、文档多对新手友好。项目结构按职责分层server/ app.js // 入口注册中间件和路由 routes/ // 路由定义一个业务模块一个文件 controllers/ // 业务逻辑层route只做转发 models/ // 数据库模型可以用Sequelize或mysql2直接写SQL middlewares/ // 鉴权、错误处理、上传等中间件 config/ // 数据库配置、常量配置 utils/ // 工具函数新手最容易犯的错就是把所有接口都写在app.js里几百行代码堆在一起改一个功能要翻半天。按模块拆分后routes/goods.js只管商品相关路由controllers/goodsController.js里实现具体逻辑后期维护和排查bug都轻松得多。数据库层我用mysql2Sequelize。Sequelize是ORM可以把表映射成模型对象查询用Goods.findAll({ where: { status: 1 } })这种方式避免手写大量SQL。如果你觉得ORM有学习成本用mysql2直接写SQL也没问题就是注意防止SQL注入——所有条件都用参数占位符?不要拼字符串。4.2 JWT鉴权中间件十分钟实现登录态控制用户端接口中需要登录才能访问的接口下单、购物车、收藏都要做鉴权。我用JWTJSON Web Token它的好处是服务端不需要存sessiontoken携带在请求头里解析就能得到用户信息非常适合前后端分离和分布式场景。生成token在登录接口里const jwt require(jsonwebtoken) // 登录成功后 const token jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: 7d } )鉴权中间件const authMiddleware (req, res, next) { const authHeader req.headers.authorization if (!authHeader || !authHeader.startsWith(Bearer )) { return res.status(401).json({ code: 401, msg: 未登录 }) } const token authHeader.split( )[1] try { const decoded jwt.verify(token, process.env.JWT_SECRET) req.userId decoded.userId req.userRole decoded.role next() } catch (error) { return res.status(401).json({ code: 401, msg: token无效或已过期 }) } }JWT_SECRET一定要放在环境变量或配置文件里不要硬编码在代码中。过期时间我一般设在7天太短影响体验、太长有安全隐患。如果你做的是包含刷新token的完整体系还可以设计成access_token2小时 refresh_token14天但对课设/毕设来说单一token七天过期完全够用。还有一点登出功能不要迷信前端删token就完事。虽然JWT无状态删token客户端确实就没了凭证但严谨的做法是在后端维护一个token黑名单存Redis登出时把当前token加入黑名单。实测下来课设阶段不做也问题不大答辩时能说清楚原理就好。4.3 图片上传multer配置和静态资源暴露商品图片上传是二手交易平台的高频功能。Express里用multer处理文件上传非常顺手const multer require(multer) const path require(path) const fs require(fs) const storage multer.diskStorage({ destination(req, file, cb) { const uploadPath path.join(__dirname, ../public/uploads) if (!fs.existsSync(uploadPath)) { fs.mkdirSync(uploadPath, { recursive: true }) } cb(null, uploadPath) }, filename(req, file, cb) { const ext path.extname(file.originalname) // 用时间戳随机数命名避免文件名冲突 cb(null, Date.now() _ Math.random().toString(36).substring(2, 8) ext) } }) const upload multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, // 限制5MB fileFilter(req, file, cb) { const allowTypes [.jpg, .jpeg, .png, .gif, .webp] const ext path.extname(file.originalname).toLowerCase() if (allowTypes.includes(ext)) { cb(null, true) } else { cb(new Error(仅支持jpg、png、gif、webp格式)) } } })上传接口返回文件URL时注意要把静态资源目录暴露给外部访问。在app.js里加一行app.use(/uploads, express.static(path.join(__dirname, public/uploads)))这样前端拿到的就是http://localhost:3000/uploads/xxxx.jpg直接能访问。这里有个容易踩的坑不要直接把上传目录放在项目根目录之外的路径因为express静态资源只暴露指定目录跨目录访问会404。我最初犯过这个错上传成功但图片加载不出来排查了半天发现是路径映射不匹配。4.4 订单状态流转一套足够应付毕设的状态机设计订单是二手平台最核心的领域模型状态流转设计得好后面的退款逻辑、商家结算逻辑都会顺很多。我的状态机状态值含义可执行操作0待付款取消、支付1待发货商家发货2待收货买家确认收货3已完成买家评价、订单完结4已取消无后续操作5退款中商家同意/拒绝退款6已退款退款完成创建订单接口的核心逻辑async function createOrder(req, res) { const { goodsId, addressInfo } req.body const userId req.userId // 1. 查询商品校验状态和库存 const goods await Goods.findByPk(goodsId) if (!goods || goods.status ! 1) { return res.json({ code: 400, msg: 商品已下架 }) } // 2. 幂等校验2分钟内同一用户同一商品已有待付款订单则直接返回 const existingOrder await Order.findOne({ where: { userId, goodsId, status: 0, createdAt: { [Op.gt]: new Date(Date.now() - 2 * 60 * 1000) } } }) if (existingOrder) { return res.json({ code: 200, data: existingOrder }) } // 3. 创建订单这里的事务非常关键防止并发超卖 const order await sequelize.transaction(async (t) { const order await Order.create({ orderNo: generateOrderNo(userId), userId, merchantId: goods.merchantId, goodsId: goods.id, goodsSnapshot: JSON.stringify({ title: goods.title, price: goods.price, cover: goods.coverImage }), amount: goods.price, status: 0, addressInfo: JSON.stringify(addressInfo) }, { transaction: t }) // 扣减库存 await Goods.decrement(stock, { by: 1, where: { id: goodsId, stock: { [Op.gt]: 0 } }, transaction: t }) return order }) res.json({ code: 200, data: order }) }**事务是这里不能省的环节。**创建订单和扣减库存必须在一个事务里执行否则一旦创建订单成功但扣减库存失败数据就不一致了。我实测中发现如果不用事务高并发场景下很容易出现库存为负或者下单成功但超卖的情况。二手平台的库存虽然通常不多但逻辑上不能留这个缺口。支付环节做模拟即可。用户点击“模拟支付”前端调用支付接口后端直接更新订单状态为待发货同时给商家生成一条站内消息通知。如果你想让答辩更有亮点可以在这里引入一个pay_log表记录支付流水然后把商家账户余额按订单金额扣掉平台抽成后的比例增加这样就把“商家结算”和“平台抽成”的闭环打通了是一个很好的加分项。5. 商家端ThinkPHP后台管理逻辑实现5.1 ThinkPHP多应用模式下如何规划模块ThinkPHP从6.0版本开始对多应用的支持很好8.0更是默认成熟。我建议商家端采用独立应用app的方式而不是在同一个应用下用控制器前缀区分。这样商家后台和用户端的路由、中间件、配置都能独立管理互不干扰。项目结构大致是这样的thinkphp/ app/ index/ // 用户端API如果你想让TP同时服务用户端的话 merchant/ // 商家端API admin/ // 平台管理端可选 config/ // 全局配置 route/ // 路由定义不过在标题“Nodejs和vue框架的二手交易平台”里用户端已经交给Node.js了所以ThinkPHP在架构里更多是承担商家端和后端管理接口的职责。你可以让商家端管理后台的页面也用Vue去渲染ThinkPHP只提供RESTful API也可以直接用ThinkPHP自带模板渲染商家后台页面。考虑到你已经在用Vue了我建议统一做前后端分离商家端页面单独用Vue写一个merchant-web请求走ThinkPHP提供的接口既能复用你已有的Vue技能也避免维护两套渲染模式。创建商家端应用php think app:create merchant然后在app/merchant/controller下按业务建控制器Goods.php、Order.php、Merchant.php店铺信息、Statistics.php数据统计。5.2 商家鉴权中间件JWT在ThinkPHP里的实践商家端接口不能复用Node.js的JWT但鉴权思路完全一致。后台接口除了商家正常操作还要做接口级权限校验——比如普通商家不能访问管理员接口商家只能操作自己的商品和订单不能越权。ThinkPHP 8的中间件定义在app/merchant/middleware.php里注册核心代码如下// app/merchant/middleware/AuthMiddleware.php namespace app\merchant\middleware; use Closure; use think\Request; use think\Response; use Firebase\JWT\JWT; use Firebase\JWT\Key; class AuthMiddleware { public function handle(Request $request, Closure $next) { $token $request-header(Authorization); if (!$token) { return json([code 401, msg 未登录]); } // 去掉Bearer前缀 $token str_replace(Bearer , , $token); try { $decoded JWT::decode($token, new Key(config(jwt.secret), HS256)); // 判断角色只允许商家或管理员访问商家端接口 if (!in_array($decoded-role, [merchant, admin])) { return json([code 403, msg 无权限访问]); } // 把商家信息绑定到请求对象上 $request-merchantInfo $decoded; } catch (\Exception $e) { return json([code 401, msg token无效或已过期]); } return $next($request); } }中间件的使用就让商家控制器直接继承一个带鉴权的基类或者在路由分组里统一挂载中间件。ThinkPHP 8路由分组写法Route::group(merchant, function () { Route::post(goods/save, Goods::save); Route::post(goods/update, Goods::update); Route::post(goods/off, Goods::off); Route::get(order/list, Order::list); })-middleware(\app\merchant\middleware\AuthMiddleware::class);JWT库在PHP端用firebase/php-jwtComposer一行安装composer require firebase/php-jwttoken的生成端虽然主要在Node.js用户端登录但商家端登录和后台登录也可以独立签发token两边互不依赖。为了统一我把jwt.secret配置在两边环境变量里保持一致这样同一个用户在前端登录后商家端接口也能从同一个token里解析出角色信息方便统一管理。5.3 商品上下架、订单处理和数据统计商家端的核心操作就三个管理商品、处理订单、看数据。ThinkPHP写这类CRUD接口效率极高因为TP自带的查询构造器非常顺手。商品管理控制器里最典型的接口是条件分页列表// app/merchant/controller/Goods.php public function list(Request $request) { $page $request-get(page, 1); $limit $request-get(limit, 10); $status $request-get(status, ); // 筛选状态 $merchantId $request-merchantInfo-merchantId; $query Goods::where(merchant_id, $merchantId) -where(is_delete, 0); // 关键词搜索标题模糊匹配 $keyword $request-get(keyword, ); if ($keyword) { $query-whereLike(title, % . $keyword . %); } if ($status ! ) { $query-where(status, $status); } $list $query-order(id, desc) -paginate([list_rows $limit, page $page]); return json([code 200, data [ list $list-items(), total $list-total(), page $page, limit $limit ]]); }商家发布商品的接口稍微多一点因为涉及商品主表写入和图片表批量写入。用事务包起来主表成功后再写子表任何一个失败都整体回滚。ThinkPHP里事务写法很简洁Db::transaction(function () use ($data, $images) { $goodsId Goods::insertGetId($data); $imageData array_map(fn($img, $i) [ goods_id $goodsId, image_url $img, sort $i ], $images, array_keys($images)); GoodsImage::insertAll($imageData); });订单处理方面商家看到待发货订单后点发货接口更新订单状态并写入物流单号。注意这里的权限校验——只能操作merchant_id等于当前商家的订单直接在查询条件里带上商家ID防止越权。退款处理同理商家可以同意或拒绝退款同意后退款状态流转同时把订单金额退回买家余额。数据统计接口我一般做三个维度今日新增订单数、今日销售额、在售商品数。一条聚合查询搞定public function overview() { $merchantId $request-merchantInfo-merchantId; $today date(Y-m-d 00:00:00); return json([code 200, data [ today_order_count Order::where(merchant_id, $merchantId) -where(status, 1) -where(pay_time, , $today) -count(), today_sales Order::where(merchant_id, $merchantId) -where(status, 1) -where(pay_time, , $today) -sum(amount), on_sale_goods Goods::where(merchant_id, $merchantId) -where(status, 1) -where(is_delete, 0) -count(), ]]); }数据统计页面用Vue的echarts画两张图近7日订单趋势、商品分类占比接口把原始数据返回前端组装图表效果非常能打。6. 环境配置与高频踩坑实录6.1 Node.js安装与环境变量配置细节Node.js安装本身不难但有几个细节决定你后续能不能顺利跑项目。去官网下载LTS版本安装包一路Next就能完成。关键是安装时注意勾选“Add to PATH”这一项如果不勾安装完在终端里执行node -v会提示找不到命令。装完验证node -v npm -v这里能看到npm版本如果npm版本太老比如低于8建议顺手执行npm install -g npmlatest升级一下。环境变量方面如果你安装时没勾选自动加PATH或者想手动管理可以找到Node安装目录默认是C:\Program Files\nodejs\把它加到系统环境变量的Path里。还有一个经验npm全局包的安装目录默认也在nodejs目录下如果你不想把所有全局包都装在C盘可以改npm的prefix配置npm config set prefix D:\nodejs\global这种方式适合对磁盘空间有要求的朋友改完后记得把新目录也加入PATH否则npm install -g安装的工具命令找不到。6.2 npm.ps1执行策略报错彻底解决而不是临时绕过说起环境配置我必须单独用一整节来聊这个报错因为太多人卡在这里了。它的典型症状是你在PowerShell里执行任何npm命令哪怕是npm -v终端劈头盖脸给你来一句npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。 有关详细信息请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Policies。报错本身不是npm坏了而是PowerShell的执行策略Execution Policy默认禁止运行.ps1脚本文件。npm命令在PowerShell里不是直接执行exe而是通过npm.ps1这个脚本包装的所以被策略拦住了。解决方案有三个按推荐程度排方案一修改执行策略推荐以管理员身份打开PowerShell执行Set-ExecutionPolicy RemoteSigned输入Y确认。RemoteSigned的含义是本机创建的脚本可以运行从网络下载的脚本必须有数字签名才能运行。这个策略既解决了npm运行问题又保留了基本安全防护也是微软官方建议的开发环境配置。方案二换成Cmd命令提示符临时方案如果你不想动PowerShell的设置直接在开始菜单搜索“命令提示符”在Cmd里执行npm命令就不会有这个问题。但这只是绕过以后用PowerShell还是会遇到不推荐日常使用。方案三用nvm管理多个Node版本进阶方案如果你有多个项目的Node版本需求建议从安装环节就换成nvm-windows。nvm的好处是可以在多个Node版本间一键切换而且它安装时会自动配置好环境变量几种终端的兼容性都处理过。实测用了nvm之后工具链相关的路径问题少很多。我个人的建议是安装了nvm就用nvm没装nvm就执行Set-ExecutionPolicy RemoteSigned解决。这两个方案配合起来一万年都不会再碰这个报错。另外如果你用了VS Code它内置终端默认就是PowerShell所以改完策略后记得完全关闭VS Code再重开让终端进程重新加载策略否则改完还是报错。6.3 前后端联调期的跨域和路径问题用户端Vue跑在5173端口Node.js服务跑在3000端口前端页面主动请求http://localhost:3000/api/xxx一定会有跨域报错。解决跨域最省心的方式是后端开CORS中间件// Node.js端 const cors require(cors) app.use(cors())开发环境下开着是完全没问题的。生产部署时如果你用Nginx做反向代理让前端和后端API处在同一域名下比如/api路径代理到Node服务跨域问题自然消失cors中间件关掉也没关系。ThinkPHP端跨域同样要处理。TP没有内置cors中间件需要自己写一个或者用TP官方的think-cors扩展。我习惯自己在app/merchant/middleware.php里加一个简单的跨域中间件核心就是设置响应头public function handle(Request $request, Closure $next) { $response $next($request); $response-header([ Access-Control-Allow-Origin *, Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS, Access-Control-Allow-Headers Authorization, Content-Type, ]); return $response; }注意浏览器预检请求OPTIONS要提前返回不要走到业务控制器否则前端的复杂请求会直接失败。实测很多人在联调阶段遇到“请求失败状态码0”的问题八成就是OPTIONS预检没处理好。路径问题则是另一个隐藏雷区。前端请求/api/goods/listNginx要把这个路径代理到Node的3000端口但Node里的路由可能是/goods/list不带/api前缀。解决办法是代理时去除前缀location /api/ { proxy_pass http://127.0.0.1:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }proxy_pass末尾带/表示把/api/替换为空请求到Node服务就变成了/goods/list。这个细节很多人忽略结果接口404还以为是代码写错了。6.4 部署上线Vue打包 Nginx PM2守护项目开发完成后面临上线部署这里给一个最简单可靠的方案。前端Vue项目打包npm run build产物在dist目录里面是静态文件。把整个dist目录上传到服务器配置Nginx托管server { listen 80; server_name your-domain.com; # 前端静态文件 location / { root /var/www/second-hand/dist; index index.html; # Vue路由使用history模式必须配置try_files否则刷新页面404 try_files $uri $uri/ /index.html; } # Node.js API反向代理 location /api/ { proxy_pass http://127.0.0.1:3000/; } # ThinkPHP商家端API location /admin_api/ { proxy_pass http://127.0.0.1:8000/; } }Node.js服务用PM2做进程守护崩溃自动重启服务器开机自启pm2 start app.js --name second-hand-server pm2 save pm2 startupThinkPHP项目如果是API服务也可以用php think run配合nohup或者Supervisor。不过更正规的方式是用Nginx直接跑PHP-FPM如果ThinkPHP暴露在独立域名或路径下但如果你只是做课设演示用php think run --host 127.0.0.1 --port 8000配一个Supervisor守护也完全够用。我这里更建议演示环境用Supervisor或PM2把ThinkPHP也守护起来避免终端一关服务就挂掉的窘境。部署时还要区分环境变量VITE_API_BASE_URL在生产环境必须指向线上域名而不是localhost。我在.env.production里这样配VITE_API_BASE_URL/api这样前端打包后所有请求走相对路径/api由Nginx代理转发彻底避免域名写死的问题。7. 个人经验总结最后再分享几个实操中换来的心得整套系统我从需求分析到部署上线跑了完整流程最大的感悟是技术栈混搭不可怕可怕的是没有划分清楚边界。Node.js和ThinkPHP各管一摊前端Vue统一对接中间靠统一的JSON返回格式和JWT串起来这个架构在毕设/课设级别已经相当完整甚至拿到小规模商用场景也撑得住。再补两个我在真实项目中后期觉得非常有用的扩展方向接入WebSocket做实时消息。用户下单后商家要马上收到通知Node.js用socket.io做服务端推送商家端Vue页面监听消息实现“下单即弹窗提醒”整个系统的交互体验会明显提升一个档次。这个点如果你在答辩时演示出来基本能拿满创新分。图片懒加载和商品推荐。二手平台商品数量一多列表页面性能就开始吃紧。Vue里用v-lazy指令或者IntersectionObserver实现图片懒加载配合后端接口做简单的热度推荐按浏览量排序开发量不大但效果显著。最后说回那个npm.ps1的报错我见过太多人因为这个卡在项目启动第一步就放弃了。环境问题永远是零门槛的手艺活别被它吓住Set-ExecutionPolicy RemoteSigned加管理员身份运行五十秒解决然后就可以安心享受写代码的快乐了。
返回列表