ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Node.js+Vue3打造AI算力资源商城:全栈设计与实现

2026/9/14 20:48:52 拓冰建站 浏览量
Node.js+Vue3打造AI算力资源商城:全栈设计与实现 1. 项目概述1.1 为什么会做一个AI算力资源商城这两年生成式AI、大模型训练、微调、推理部署的项目越来越多不管是做独立的AI产品还是企业内部做智能应用都绕不开GPU算力。但实际接触一圈就会发现个人开发者或者中小团队想买算力渠道其实挺难受的要么去公有云官网按实例下单页面复杂、计费项多看着头晕要么找第三方渠道租赁但价格不透明、交付周期看运气还有很多人用的是共享GPU邻居一个任务就能把显存挤爆训练中途直接OOM。所以当时我给自己定了一个目标做一个面向AI开发和科研场景的算力资源商城系统把GPU云服务器、训练任务、推理API这类数字商品像普通电商一样上架、选购、下单、支付、自动交付。用户打开网页明码标价看到不同显卡配置选好规格和时长下单选卡系统自动开通资源全程像逛淘宝一样顺畅。这个项目我用的是Node.js Vue3这套技术栈前后端分离整体做下来无论是业务建模还是工程落地都有不少值得复盘的地方。这篇文章就完整拆解一下这个系统的设计与实现包括技术选型、数据模型、核心模块、前端交互、部署避坑以及我踩过的比较典型的坑。适合正在做全栈项目、想了解数字商品交易系统如何设计、或者准备把AI算力资源平台化的开发者参考。如果你是刚学完Node.js和Vue3想找个综合项目练手这篇文章同样可以参考。2. 整体设计与技术选型分析2.1 为什么选Node.js而不是Java或Go后端选型的时候我其实纠结过一阵毕竟市面上主流的电商系统几乎都是Java Spring Cloud的天下Go在云原生领域也很有话语权。但考虑到这个项目的实际定位最后我还是定了Node.js。一个核心原因是团队技术栈统一。前端用Vue3必然要写大量JavaScript/TypeScript如果后端也统一到Node.js上那前后端可以共享类型定义、接口契约、工具函数联调成本会低很多。比如我封装了一个createApiClient前端和后端引用同一份.d.ts类型声明改接口字段时编译期就能暴露问题这一点在快节奏迭代中非常香。另一个原因是Node.js在I/O密集场景的表现足够好。算力商城这个业务大量的操作其实是“转发”和“编排”用户下单系统要调用云服务商API去开通实例、查询状态、推送日志还要在WebSocket上推送实时进度。这些都是典型的I/O密集型任务Node.js的事件循环模型天然适合。实际压测下来单机Node.js处理2000个并发下单请求没有出现瓶颈对商城项目来说完全够用。当然如果业务规模真的到了千万级订单量要上分布式事务、复杂金融级对账那Java和Go的生态确实更成熟。但那是后话对于起步阶段的算力资源商城Node.js能让我用最小的成本跑通整个业务闭环这就够了。2.2 为什么前端选择Vue3 Vite Pinia前端用Vue3几乎是顺理成章的选择。Vue3的组合式APIComposition API比Options API更适合商城这种功能复杂的业务页面因为它可以把同一个业务维度的逻辑聚合在一起。举个例子购物车页面里计价、优惠、库存校验、结算按钮状态这四块逻辑在Options API里可能要分散到data、computed、methods三个区域但在组合式API里我可以封装一个useCart组合函数所有购物车相关状态和操作一目了然。工程化这块我用的是Vite。Vite的开发服务器基于原生ES Module冷启动速度比Webpack快一个数量级改代码后的热更新也是毫秒级。这个项目组件数量上百如果用Webpack每次保存都要等两三秒开发体验会差很多。而且Vite对Vue3的支持是官方一等公民插件生态也齐全不需要额外的兼容层。状态管理我选的是Pinia。Vuex 3.x是为Vue2设计的Vuex 4虽然支持Vue3但设计上还是老一套Pinia则完全按照Vue3的响应式原理重写去掉了mutations的概念直接在action里修改state代码量少了接近一半。在我这个商城项目里登录用户信息、全局购物车数量、资源开通状态这些跨页面共享的状态用Pinia管理非常顺手。2.3 商城系统的整体架构和模块边界整个系统采用前后端分离架构前端部署在Nginx静态服务器上后端是Node.js服务数据层用MySQL Redis。前端 Vue3 Vite Pinia Element Plus | |--- HTTP/WebSocket --- | 后端 Node.js Express TypeScript | |--- ORM --- MySQL商品、订单、用户 | |--- Redis购物车缓存、分布式锁、验证码 | |--- 云服务商API开通实例、查询状态 | |--- 对象存储商品图片、资源快照后端按业务域拆成六个模块用户模块注册登录、实名认证、API密钥管理、商品模块算力规格管理、库存状态、价格策略、订单模块下单、计价、支付回调、交付模块实例开通、状态同步、销毁释放、钱包模块余额充值、流水记录、发票申请、运维模块后台管理、日志、告警。这个模块边界的划分是我比较满意的部分。传统电商项目容易把“订单”和“交付”揉在一起但算力商城不一样支付完成后交付是异步的可能需要几十秒甚至几分钟才能把GPU实例开通好所以订单和交付必须彻底解耦用订单状态机来驱动整个流程。2.4 为何把算力资源设计成可上架的数字商品一开始我考虑过两种商品模型。第一种是把算力资源当成服务用户提交工单申请管理员线下开通后告知用户连接信息第二种是把算力资源当成标准化数字商品用户在线选择规格、自动计价、自助下单、系统自动交付。第一种模型的开发量小很多但使用体验太差。用户下单后不知道要等多久也不知道进度管理员手工开通容易出错高峰期容易漏单。第二种模型虽然前期开发成本高但一旦跑通用户不需要跟任何人沟通从下单到拿到SSH连接信息全是自动化的这是成了一个平台该有的样子。最后我选择了第二种并且设计了一套适配数字商品的SKU模型。举一个例子一张“RTX 4090云主机”的商品卡片在数据库里不是一个简单字段而是一组参数的组合{ id: sku_4090_24g, name: RTX 4090 云主机, spec: { gpu_type: RTX 4090, gpu_count: 1, vram_gb: 24, cpu_core: 8, ram_gb: 64, disk_gb: 200, bandwidth_mbps: 100, region: cn-east-1 }, settlement: { charging_mode: hourly, price_per_hour: 3.5 }, stock: { total: 20, available: 13 } }商品的基础规格用spec字段保存SKU之间通过规格差异来区分。这样用户在商品详情页选GPU型号、选区域、选时长时前端可以根据spec的组合动态计算价格后端根据stock.available做库存扣减逻辑既不复杂又能适配未来扩展更多算力型号。3. 数据模型与核心业务设计3.1 数据表设计与商品SKU模型数据库设计是整个系统的地基这块我前前后后改了四版才算稳定。主要核心表如下user用户表包含手机号、邮箱、密码哈希、余额、状态等字段。gpu_product算力商品表类似SPU标准产品单元比如“RTX 4090云主机”“A100训练集群”。gpu_sku货品表类似SKU库存量单位关联SPU并通过参数区分不同配置。cart购物车表记录用户加购的SKU、数量、时长、规格快照。order订单主表记录订单号、用户ID、总金额、优惠金额、实付金额、订单状态。order_item订单明细表关联具体SKU、价格、时长、规格快照。instance算力实例表记录交付出来的云主机信息公网IP、SSH端口、系统镜像、到期时间。delivery_task交付任务表记录下单后每个实例的开通进度和状态。wallet_transaction钱包流水表记录充值、消费、退款等资金变动。商品和SKU为什么要分开两张表因为一个商品下会挂多个SKU比如“RTX 4090”下面有“按小时计费”和“包月计费”两个SKU它们的用户价值本质上是一个商品但不通规格的库存和价格都不同。SPU和SKU分离之后商品详情页的整体信息存放在SPU表SKU表存具体规格和库存后端通过SPU ID一次性查出所有SKU前端渲染会非常高效。3.2 订单状态机与关键状态流转订单状态是整个系统的核心枢纽这个状态机设计得好不好直接决定了后续开发和排障的复杂度。我的订单状态流转如下待支付 - 支付成功(待交付) - 交付中 - 运行中 - 已过期/已释放 | | | --- 已关闭用户取消 ------- 已关闭超时未支付状态之间是单向不可逆的任何状态变更都必须有对应的操作记录日志。即使同一个状态位被错误提交因为有前置状态校验也不至于跳到不该去的地方。这里有个小细节订单超时关闭用的是Redis延迟队列而不是每秒扫表。用户下单后会拿到一个订单号同时在Redis的order_expire有序集合里写入过期时间戳后台起一个定时任务每10秒扫描一次把过期未支付的订单关掉并释放购物车库存。这个方案比每秒全表扫描订单表效率高太多了也不会给MySQL带来无谓压力。3.3 计价策略与分布式锁防超卖算力资源的计价比普通商品复杂不少。普通商品是单价乘以数量算力资源还要乘以时长并且时长越久折扣越大。我设计了三个计价参数基础单价按小时计费的单价时长系数按天计费是小时价的22倍左右相当于约9折按月是小时价的480倍左右相当于约67折阶梯折扣一次性付费满一定金额后打折最终计算公式为总金额 基础单价 × 时长(小时) × 时长系数 × 阶梯折扣折扣逻辑由后端服务统一计算前端展示的实时价格只是调用后端的计价接口避免前端改价、下单金额和实际支付金额不一致的问题。所有订单金额到元为止采用toFixed(2)后存储DECIMAL(10,2)类型。防超卖是商城系统的经典问题我用Redis的DECR命令做库存扣减。下单时先把SKU预扣DECR sku_stock:{skuId}如果返回负数说明库存不足直接提示用户并回补。真正支付成功后才走正式的库存扣减超时未支付则回补。这个方案的优点是原子性有保障不会出现两个请求同时读到库存为1然后都下单成功的情况。3.4 交付模块从订单到算力实例的自动闭环这是整个系统最特别的地方也是算力商城和普通商城最大的区别。普通商品支付完就完事了算力资源支付完之后还有个“生产”过程——后端要去云服务商那边开通一台真正的云主机然后才能把连接信息返回给用户。交付模块我设计成了类似的异步任务流水线用户支付成功订单状态变为“待交付”。后端创建delivery_task状态为pending。交付任务调度器取出任务调用云服务商API创建云主机实例。轮询查询实例状态等它变为running后获取IP、SSH端口和初始密码。更新instance表写入连接信息把订单状态变为“运行中”。如果中途失败任务重试最多3次仍失败的进入死信队列由运维人员报警介入。这个过程中最坑的是云服务商API开通实例的响应时间不稳定短则30秒长则5分钟。所以我用了异步任务轮询而不用HTTP同步等待否则用户的支付请求一直卡在那里Nginx反代都要超时了。前端通过WebSocket接收交付进度推送页面实时展示“开通中 40%”之类的内容互动感好很多。4. 核心功能模块实现与关键代码4.1 用户注册登录与JWT鉴权用户模块用了JWT做无状态身份认证。注册时对密码做bcrypt哈希存储登录成功后签发Access Token和Refresh Token。Access Token有效期2小时Refresh Token有效期7天前端把Token存在内存变量和localStorage里每次请求在Axios拦截器里带上Authorization: Bearer token。// 后端核心代码JWT签发与验证 import jwt from jsonwebtoken; const JWT_SECRET process.env.JWT_SECRET || your-secret; export function signToken(payload: Recordstring, any, expiresIn: string) { return jwt.sign(payload, JWT_SECRET, { expiresIn }); } export function verifyToken(token: string) { try { return jwt.verify(token, JWT_SECRET); } catch (error) { return null; } }有一个我踩过的坑JWT是无状态的一旦签发在有效期内后端无法主动让它失效。如果用户想退出登录单纯前端删掉Token原来的Token其实还是可用的。所以我额外在Redis里维护了一个黑名单退出登录时把Token的jti唯一ID写入黑名单并设置和Token一致的过期时间每次请求校验Token时先查一下黑名单。虽然增加了一次Redis查询但安全性提升很多。4.2 商品列表与SKU动态选择商品列表页是用户了解算力资源的第一入口。前端通过后端接口拉取SPU列表和对应的SKU列表渲染成商品卡片。用户点进详情页后会看到GPU型号、显存、CPU核数、内存、带宽、区域等多个可选项这些选项组合起来就是一套SKU。SKU选择器的核心逻辑是每次用户更改某个规格选项都要重新计算当前可选的规格组合。简单说如果我锁定了“RTX 4090”和“cn-east-1”那“按小时”“按天”“包月”这三个计费方式如果都有货就全部可选如果“按天”对应的SKU库存为0则置灰不可选。!-- 前端Vue3核心代码SKU选择器简版 -- script setup langts import { computed } from vue; const props defineProps{ skus: SkuItem[]; selected: Recordstring, string | number; }(); const emit defineEmits{ (e: change, payload: Recordstring, string | number): void; }(); // 计算当前已选规格组合 const currentSku computed(() { return props.skus.find((sku) { return Object.entries(props.selected).every( ([key, value]) sku.spec[key] value ); }); }); function selectOption(key: string, value: string | number) { emit(change, { ...props.selected, [key]: value }); } /script这里的技巧在于SKU匹配不能简单用整个对象对比因为selected可能只锁定了一部分维度还有维度没选定。所以判断“当前选中是否构成一个有效SKU”时要用every逐个字段比对只要所有已经选定的维度都匹配就算是一组候选组合。当前Sku一旦匹配成功就可以动态计算总价、库存状态和“立即购买”按钮的可点击状态。4.3 购物车与实时计价购物车的设计参考了主流电商的做法未登录用户用本地存储localStorage保存购物车登录后可以把本地购物车合并到服务端购物车。合并时以服务端购物车为基准但本地新增且服务端不存在的SKU要追加进去。购物车实时计价是比较考验前端响应式和后端接口配合的。购物车列表里每一项都有“购买时长”这个输入而价格又是按小时单价乘以时长计算的所以只要用户改一个时长整个购物车的汇总金额就要重新计算。我是这样实现的// 前端Vue3核心代码购物车汇总计价 import { computed, ref } from vue; const cartItems ref([]); const totalAmount computed(() { return cartItems.value.reduce((sum, item) { const lineAmount item.unitPrice * item.quantity * item.hours; return sum lineAmount; }, 0); });减少不必要的渲染这里用到了computed的缓存特性。购物车组件里多个区域都在引用totalAmount但因为它是响应式依赖只有cartItems变化时才会重新计算并不会因为组件重渲染而重复执行。实际渲染数量几十个SKU时完全无压力。服务端购物车在Redis里用Hash存储每个SKU对应一个字段值是JSON字符串包含数量、时长、规格快照。下单时直接读取整个Hash转为订单明细然后清空购物车。用Hash的好处是用户可以快速更新单个SKU的购买时长而不需要动整个购物车对象。4.4 支付流程与回调签名校验支付是这个项目里代码量最大、坑也最多的一个模块。由于是个人项目我没有接那种需要企业资质才能开通的支付渠道而是用了一个模拟支付网关但整个流程完全参考真实支付系统的回调规范。支付流程如下用户提交订单后端创建订单并返回订单号和支付参数。前端跳转到模拟收银台页面用户点击“确认支付”。模拟网关收到支付请求生成一笔待确认的交易然后向商城后端发送异步通知回调。后端收到回调验证签名和金额确认无误后更新订单状态为“已支付”并触发交付任务。前端通过WebSocket或轮询感知订单状态变化跳转到“支付成功”页面。签名校验是支付安全的核心。模拟网关用商户密钥对order_id、amount、timestamp生成一个MD5签名回调时后端用同样的密钥和字段顺序重新计算签名如果算出的签名和回调里的签名不一致直接拒绝这笔回调。还要校验金额严格一致防止有人伪造回调说付了钱。// 后端核心代码支付回调签名校验 function verifySign(params: Recordstring, string, sign: string): boolean { const secret process.env.PAY_SECRET || ; const filtered Object.keys(params) .filter((key) key ! sign) .sort() .map((key) ${key}${params[key]}) .join(); const expected crypto.createHmac(md5, secret).update(filtered).digest(hex); return expected sign; }注意这里有一个细节参与签名的参数必须排序否则字段顺序变了签名结果就不同导致真实的回调也被误判为伪造。我用的方式是按参数名的字母序排序后再拼接这是业内常见的做法你们做真实支付对接时也要遵循商户平台给的签名规则。同时回调处理接口必须实现幂等。支付网关可能会因为网络重试给同样的回调发多次如果每次收到回调都去更新订单状态第二次就会把状态从“已支付”改成“已支付”虽然不报错但白白造成一次多余的update。更重要的是如果出现并发回调两个请求同时到了可能都会判定可以进入交付流程导致交付任务被创建两次。所以我在处理回调时用了数据库唯一订单号作为分布式锁的key只有获取到锁的请求才能执行状态流转另一个请求直接返回“已处理”。4.5 算力实例管理与到期自动释放用户支付完成后拿到了实例连接信息接下来就是实例管理查看实例状态运行中/已停止/已到期、连接信息、监控数据、续费、强制关机、销毁释放。这个模块的核心是到期释放。我在实例表里维护了expire_at字段后台跑一个定时任务每分钟扫一次把到期的实例执行释放操作先通知云服务商销毁实例然后更新本地状态为“已释放”同时生成一笔钱包流水记录资源退回的押金如果有。有一个业务细节到期释放前7天、3天、1天各发一次站内信和邮件提醒避免用户资源突然没了影响实验。这个提醒功能用了一个简单的定时任务扫描expire_at减去当前时间落在提醒时间窗口内的实例然后异步推送消息。一开始我是在每个用户请求时顺带检查有没有到期提醒后来发现数据量大了效率不行改成定时任务后好多了。5. 前端Vue3实现与体验优化5.1 组合式API组织商城业务逻辑商城前端的业务逻辑非常复杂如果全部堆在组件里代码很快就没法维护了。我的做法是用组合式函数Composable把业务逻辑拆出来组件只负责模板渲染和事件绑定。以商品详情页为例我封装了useProductDetail、useSkuSelector、usePricing三个组合函数。useProductDetail负责拉取商品信息、评论列表、关联推荐useSkuSelector管理SKU选择状态和可用性计算usePricing监听SKU选择结果调用后端计价接口返回实时价格。// 组合式函数示例usePricing import { ref, watch } from vue; export function usePricing(selectedSku, quantity, hours) { const price ref(0); const loading ref(false); async function fetchPrice() { if (!selectedSku.value) return; loading.value true; try { const { data } await api.get(/api/sku/price, { params: { skuId: selectedSku.value.id, quantity: quantity.value, hours: hours.value, }, }); price.value data.price; } finally { loading.value false; } } watch([selectedSku, quantity, hours], fetchPrice, { immediate: true }); return { price, loading }; }这种组织方式的收益在后期维护时非常明显。比如我想在商品详情页新增一个“邀请好友立减”的活动只需要新建一个usePromotion组合函数然后在组件里调用并绑定到现有价格面板上完全不用动原来的计价逻辑。组件代码保持轻薄问题定位也快。5.2 组件化设计SKU选择器、资源卡片、订单状态标签组件化的核心目标是复用但这个项目里我最大的体会是拆分组件不是越多越好而是要把“变化点”和“稳定点”分开。商城页面的变化点主要集中在商品展示形式、SKU交互方式、订单状态展示样式上这些我抽成了独立组件而变化少的用户信息、页面框架就尽量保持稳定不强行拆分。SKU选择器是我花最多心思的组件。它的交互复杂度在于规格联动和可用性置灰。比如用户选了“RTX 4090”后区域下拉里“cn-east-1”有货但“cn-west-1”没货后者就要置灰并提示“该区域库存不足”。这个逻辑需要后端返回每个SKU的库存状态前端构建一个规格映射每次选择变化时重新计算可选项。资源卡片组件也做了适配。商品列表页的资源卡片和用户控制台的实例卡片展示的数据结构不同但外观风格要保持统一。我让资源卡片组件接收一个typeprop值为product时渲染库存和价格值为instance时渲染运行状态和到期时间。这样一套组件两处用没有复制粘贴任何模板代码。订单状态标签组件我做了更细的拆分。不同状态待支付、已支付、交付中、运行中、已过期要显示不同的颜色、图标和辅助文案。这个组件不承载任何业务逻辑只根据status字段映射样式和文案纯粹是一个展示组件好处是全站订单状态展示风格统一后续就算改配色也只动一个文件。5.3 购物车与订单确认页的响应式体验购物车和订单确认页是全项目交互最复杂的页面因为它们涉及大量“选择联动”。用户在订单确认页选择支付方式、输入优惠码、调整购买时长页面底部的支付金额面板要实时刷新切换支付方式时如果选择了余额支付还要显示当前余额并校验是否足够。响应式的核心设计是订单确认页维护一个orderDraft对象包含SKU列表、数量、时长、优惠码、支付方式等所有订单参数页面所有区域的数据都从orderDraft派生计算任何交互都只是修改orderDraft的某个字段。// 前端核心代码订单确认页响应式结构 const orderDraft reactive({ items: [], couponCode: , paymentMethod: balance, remark: , }); const payableAmount computed(() { const subtotal orderDraft.items.reduce( (sum, item) sum item.linePrice, 0 ); const discount couponDiscount.value; return Math.max(0, subtotal - discount); }); const canSubmit computed(() { return ( orderDraft.items.length 0 payableAmount.value 0 paymentValid.value ); });这里有个很关键的体验点金额计算必须完全由前端承担还是后端兜底前端的实时计算只是展示式的真正的扣费以后端订单数据为准。但前端计算出错会给用户带来困惑比如后端说优惠10元前端显示优惠5元。所以我的优惠码计算逻辑前后端是同一套算法按百分比或固定金额后端是唯一权威前端只是用相同逻辑做即时反馈。提交订单时后端的完整计价会覆盖前端的临时计算结果。5.4 交付进度实时展示与WebSocket连接支付成功后的交付过程通常需要几十秒如果让用户干等一个“处理中”的转圈体验很差。我接入了WebSocket在创建订单时前端建立连接并订阅订单ID对应的频道后端交付任务每推进一个阶段就推送一条进度消息如下前端更新进度条、步骤条和日志区域。{ type: DELIVERY_PROGRESS, orderId: 202501081234560001, stage: CREATING_INSTANCE, stageName: 正在创建云主机, progress: 40, message: 已调用云服务商API等待实例启动... }WebSocket连接管理有几个细节需要注意。一是连接要按订单绑定用户同时下了多个订单收到的消息不能串台二是连接断开后要自动重连避免用户等待到一半收到不了进度三是用户离开订单详情页时必须主动关闭连接否则连接数会越积越多。我用Vue3的onUnmounted钩子里关闭连接同时后端对长时间空闲的连接做了心跳检测超时自动断开。// 前端核心代码WebSocket连接管理简版 const ws refWebSocket | null(null); let reconnectTimer: number | null null; function connect(orderId: string) { const protocol location.protocol https: ? wss : ws; ws.value new WebSocket(${protocol}://${location.host}/ws/order/${orderId}); ws.value.onmessage handleMessage; ws.value.onclose () { reconnectTimer window.setTimeout(() connect(orderId), 3000); }; } function handleMessage(event: MessageEvent) { const data JSON.parse(event.data); if (data.type DELIVERY_PROGRESS) { progress.value data.progress; stageName.value data.stageName; } }5.5 倒计时、轮询与内存泄漏排查商城系统里有大量定时操作比如支付倒计时、交付进度轮询、优惠活动倒计时。这些功能做起来简单但最容易出问题的是内存泄漏和定时器没清理。Vue3组件卸载后如果一个setInterval没有清除它会永远运行下去持续执行回调里的DOM操作轻则报警告重则页面卡顿。我封装了一个useCountdown组合函数在组件卸载时自动清理定时器import { onUnmounted, ref } from vue; export function useCountdown(seconds: number) { const left ref(seconds); const timer refnumber | null(null); function start() { if (timer.value) clearInterval(timer.value); timer.value window.setInterval(() { left.value - 1; if (left.value 0) { clearInterval(timer.value as number); timer.value null; } }, 1000); } onUnmounted(() { if (timer.value) clearInterval(timer.value); }); return { left, start }; }另一个坑是轮询接口没有加“幂等请求”保护。比如交付中状态每5秒请求一次查询进度如果一次请求的响应时间超过5秒就会造成多个并发的查询请求同时在跑服务端压力大返回的数据也可能乱序。我在Axios层加了全局的重复请求取消机制当同一URL和相同参数的请求还在等待响应时新发起的同参数请求会被忽略或取消只在完成之后才允许下一次请求。6. 环境配置、部署上线与常见问题排查6.1 Node.js与Vue3环境配置这个项目是Node.js 18和Vue3组合环境配置本身不复杂但对于不少初学者来说第一步就容易被拦住。Node.js安装时要注意安装路径不要带中文和空格否则后续npm install和node-gyp编译原生模块会各种报错。我见过太多人把Node装到C盘带空格的路径里然后npm报权限错误折腾半天。安装完成后验证环境node -v npm -v如果版本号正常显示就说明Node.js和npm都装好了。Vue3项目用Vite创建不需要全局装Vue CLI直接使用npm来创建npm create vitelatest ai-compute-mall -- --template vue-ts cd ai-compute-mall npm install npm run dev这里有一个高频问题npm在Windows PowerShell下会报“不识别npm命令”或“因在此系统上禁止运行脚本”的错误。如果输入npm后提示“无法加载文件...ps1因为在此系统上禁止运行脚本”这是PowerShell的执行策略问题不是npm没装好。解决办法是管理员权限打开PowerShell执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser这个命令的意思是允许本地脚本运行远程未经签名的脚本仍然会拒绝安全性是可控的。改完后重新打开终端npm命令就能正常使用了。6.2 前后端API联调与跨域配置开发环境前后端分离前端跑在5173端口后端跑在3000端口必然遇到跨域问题。解决跨域最规范的方式不是在前端代理里把API地址写死而是让后端正确配置CORS中间件。// 后端核心代码CORS配置 import cors from cors; const allowedOrigins [ http://localhost:5173, https://your-domain.com, ]; app.use( cors({ origin: allowedOrigins, credentials: true, }) );如果前端请求里带了Cookie或者Authorization头后端就必须设置credentials: true并且origin不能使用*通配符必须明确指定允许的来源。这是很多新手容易踩的坑配置*后普通接口没问题但带鉴权的请求会被浏览器拦截。生产环境的反替代方案是前端构建成静态文件部署在Nginx后端跑在Node进程里Nginx把/api路径反向代理到后端服务这样前后端同源不需要CORS。配置如下server { listen 80; server_name your-domain.com; root /var/www/ai-compute-mall/dist; index index.html; location /api { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }6.3 生产环境部署PM2守护与HTTPS生产环境我用PM2守护Node.js后端进程。PM2的优势是进程崩溃自动重启、支持集群模式多实例运行、自带日志管理和监控面板。启动命令pm2 start dist/index.js --name ai-compute-mall --instances 2 pm2 save pm2 startup--instances 2表示启动两个Node进程利用多核CPU提升并发能力。这里要注意如果应用里使用了Redis或者数据库连接池多进程模式不会自动共享这些连接需要合理配置连接池大小避免每个进程都维护一堆空闲连接把数据库打满。我实际用的是连接池上限20两个进程足够用了。HTTPS证书我用的Lets Encrypt免费证书Nginx配置好证书记录后每三个月自动续期一次。对商城系统来说HTTPS是强制项而不是可选项因为用户的登录凭证、支付信息都通过加密通道传输明文HTTP很容易被中间人截获。另外现代浏览器的navigator.geolocation、crypto.subtle等API也只有在HTTPS环境下才能正常调用将来如果要接入指纹识别或者密码学相关的功能没有HTTPS会有很多限制。6.4 典型报错排查思路与解决记录项目开发过程中我记录了一堆报错挑几个最常见的分享出来你们如果遇到类似问题可以少走弯路。第一个是npm install报ERR_OSSL_EVP_UNSUPPORTED。这个问题发生在Node.js 17版本下安装老依赖时Webpack 4等工具使用了OpenSSL 3.0已经移除的算法。解决办法有两个升级依赖到兼容版本或者临时用NODE_OPTIONS--openssl-legacy-provider npm run dev绕过。前者是长久之计后者只能应急。第二个是跨域请求带不上Cookie。前面提到的credentials: true配置和Axios的withCredentials必须同时设置缺一个都会导致浏览器丢弃Cookie。我在后端CORS配置正确但前端忘了加withCredentials时排查了很久。第三个是WebSocket连接频繁断开。关键原因是我在WebSocket握手时用了自定义的Sec-WebSocket-Protocol子协议头但Nginx没有转发这个头导致浏览器认为协议协商失败。解决办法是给Nginx的location加以下配置location /ws/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }下面整理成问题速查表方便你们直接查阅问题现象可能原因解决方案npm命令无法加载ps1脚本PowerShell执行策略限制执行Set-ExecutionPolicy RemoteSignednpm install报OpenSSL错误Node.js 17与旧依赖冲突升级依赖或加--openssl-legacy-provider跨域带不上CookieCORS配置或withCredentials缺失两边同时开启credentials支持WebSocket秒断Nginx未转发Upgrade头配置proxy_set_header Upgrade和Connection数据库连接数打满多进程模式连接池过大调低连接池上限合理分配实例数订单重复支付回调回调接口未做幂等用分布式锁或唯一约束保证幂等实例到期未自动释放定时任务未覆盖该实例检查expire_at索引和任务调度日志7. 项目迭代方向与个人踩坑总结7.1 后续可以扩展的功能方向这个算力商城目前已经把交易闭环跑通了但距离一个成熟的算力平台还有很多可以扩展的地方。第一个方向是增加算力任务编排能力比如用户下单后不仅拿到一台裸机还能自动配置好Python环境、PyTorch框架、挂载数据集这在应用层做起来就是交付模块里多加几个配置步骤。第二个方向是账单与成本分析。用户一个月下来用了几百块钱算力目前只能看订单流水如果增加“按项目”“按时间段”“按实例类型”维度的账单报表对团队用户会有很大吸引力。这部分的底层是钱包流水表的聚合查询扩展起来并不难。第三个方向是市场化的算力共享。大多数GPU在夜间利用率很低而另外一个用户可能在夜间有紧急训练任务。如果把闲置算力挂到商城里做“夜间特惠场”既是商品运营的一种玩法也符合低碳计算的价值理念。搞一个“夜间折扣时段”的价格策略就能实现核心逻辑就是计价模块里多算一个时段系数。7.2 整个项目做下来我最真实的几个体会第一个体会是商城系统的难点永远不在“增删改查”而在于状态机、并发、幂等、一致性这些看不见的地方。订单状态流转稍微疏忽一点就会出现“支付了但交付不了”“交付了但状态没更新”的情况。所以开发过程中一定要把所有状态变更点都集中到Service层统一管理不要在前端或者Controller层随意改订单状态。第二个体会是数字商品和实体商品在交易系统设计上的差异很大。实体商品要考虑库存、物流、签收数字商品要想的是交付、启停、计费周期。这个算力商城让我对“交付”这个词有了更深的理解——交付不是发货而是一个服务从无到有的过程它本身就可以做成一个可视化的产品功能。第三个体会是技术选型没有绝对的最好只有适不适合当前业务阶段。Node.js Vue3这套组合在个人开发和中小团队场景下非常顺手开发效率高、生态完善、前后端语言统一但在超大规模和超高并发场景下确实需要考虑更复杂的技术方案。关键是你要清楚自己项目当前的核心矛盾是什么然后选能解决这个矛盾的方案而不是盲目跟风追新技术。最后说一个运维上的小技巧算力商城这类系统的后台定时任务续费检查、到期提醒、实例释放一定要有日志和监控面板至少要做到每次任务执行后记录任务ID、扫描记录数、成功数和失败数。我之前就是因为定时任务静默失败导致几个实例到期后一直没释放白白多跑了好几天费用账单出来才反应过来。后来我给所有定时任务加了异常告警每次执行完把关键指标打到日志系统再也没出现过这种“静默事故”。