Node.js集成Shippo跨接口获取shipment.object_id及报错排查
问题背景
- 后端基于Node.js开发,当前正在集成Shippo物流服务,已成功实现
shippo.shipment.create {...}运单创建逻辑,通过console.log(shipment)可正常获取返回的shipment.object_id - 核心需求:开发GET接口供客户选择偏好的配送费率,需要在POST创建运单的接口作用域外获取
shipment.object_id,进而查询配送费率返回给客户 - 已尝试方案:在用户Schema中新增shippingId字段,计划将获取到的
shipment.object_id存入MongoDB数据库,对应实现代码如下:
.then(function(shipment) { const shippingId = shipment.object_id; const userId = to.userId; const user = await User.findById(userId); if (user) { user.shippingId = shippingId; } const updatedUser = await user.save(); }
- 运行上述代码时报错:
SyntaxError: Unexpected reserved word - 当前编写的Shippo路由完整实现代码如下:
import express from 'express'; import shippo from 'shippo'; import expressAsyncHandler from 'express-async-handler'; const shippoToken = env.process.SHIPPO_TOKEN; const shippoClient = shippo(shippoToken); const shippoRouter = express.Router(); shippoRouter.post( '/', expressAsyncHandler(async (req, res) => { const from = req.body.address var addressFrom = { "name": from.name, "street1": from.street1, "city": from.city, "state": from.state, "zip": from.zip, "country": from.country, }; const to = req.body.name var addressTo = { "name": to.name, "street1": to.street1, "street2": to.street2, "city": to.city, "state": to.state, "zip": to.zip, "country": to.country, "phone": to.phone, "email":to.email, "validate": true, }; var parcel = { "length": "5", "width": "5", "height": "5", "distance_unit": "in", "weight": "1.5", "mass_unit": "lb" }; shippoClient.shipment.create({ "address_from": addressFrom, "address_to": addressTo, "parcels": [parcel], "async": false }) .catch(function(err) { // Deal with an error console.log("There was an error creating customs declaration: %s", err); }).then(function(shipment) { const shippingId = shipment.object_id console.log(shipment) console.log(shippingId) }) })); shippoRouter.get ( '/shippingInfo', expressAsyncHandler(async (req, res) => { // shippo.shipment.retrieve(shipment.object_id); }));
问题原因与修复方案
报错根因
SyntaxError: Unexpected reserved word 报错原因很直接:await关键字只能在被async标记的函数内部使用,你之前把await写在了.then()的普通function回调里,语法层面就不支持,所以直接抛错。
除了这个语法错误,现有代码还有几个会导致运行异常的问题:
- 环境变量读取顺序写反:
env.process.SHIPPO_TOKEN应该改成process.env.SHIPPO_TOKEN - 接口参数取值错误:
const to = req.body.name逻辑不对,正常应该从请求体取收件人地址对象,否则后续读to.name等字段会直接报空指针 - 异步逻辑没有等待:Shippo创建运单是异步IO操作,现在的写法没有等待创建完成就会结束请求,前端拿不到返回结果
- 链式调用顺序错误:
.catch().then()的写法会导致异常捕获不全,正常应该先写.then()再跟.catch(),或者直接用try/catch配合async/await - 用户ID取值风险:不要从请求体里传userId,要从登录态中间件解析当前用户ID,避免用户篡改参数操作其他人的数据
跨接口获取shipment.object_id的实现思路
把shipment.object_id持久化存储的思路是对的,但不建议直接存在用户表单字段里——一个用户会产生多个运单,单字段存储会覆盖历史运单ID,最好单独建一个运单集合存储,关联用户ID、运单ID、费率信息、创建时间等字段。
另外注意:创建运单时传了async: false,Shippo会同步返回运单结果,其中自带rates字段包含所有可用配送费率,不需要再额外调用retrieve接口二次查询,直接存库就能用,能减少一次API请求开销。
如果服务是多实例部署,绝对不要用全局内存变量存运单ID,进程重启、请求负载到不同实例都会导致数据丢失,必须存在MongoDB这类持久化存储里。
修正后的核心代码参考
import express from 'express'; import shippo from 'shippo'; import expressAsyncHandler from 'express-async-handler'; // 修正环境变量读取顺序 const shippoToken = process.env.SHIPPO_TOKEN; const shippoClient = shippo(shippoToken); const shippoRouter = express.Router(); shippoRouter.post( '/', expressAsyncHandler(async (req, res) => { // 修正参数取值,根据实际前端传参结构调整 const from = req.body.addressFrom; const to = req.body.addressTo; // 从登录态取当前用户ID,避免篡改 const userId = req.user._id; const addressFrom = { "name": from.name, "street1": from.street1, "city": from.city, "state": from.state, "zip": from.zip, "country": from.country, }; const addressTo = { "name": to.name, "street1": to.street1, "street2": to.street2, "city": to.city, "state": to.state, "zip": to.zip, "country": to.country, "phone": to.phone, "email":to.email, "validate": true, }; const parcel = { "length": "5", "width": "5", "height": "5", "distance_unit": "in", "weight": "1.5", "mass_unit": "lb" }; try { // 直接await创建结果,避免回调作用域问题 const shipment = await shippoClient.shipment.create({ "address_from": addressFrom, "address_to": addressTo, "parcels": [parcel], "async": false }); const shippingId = shipment.object_id; // 持久化存储运单ID,推荐新建Shipment模型存全量运单信息 // 临时存在用户表的写法,外层是async函数可直接使用await const user = await User.findById(userId); if (user) { // 多运单场景建议改成数组格式push,不要直接赋值覆盖 user.shippingId = shippingId; await user.save(); } // 直接返回运单ID和可用费率,前端可直接进入选择费率流程 res.send({ success: true, shippingId: shippingId, rates: shipment.rates }) } catch (err) { console.log("创建运单失败: %s", err); res.status(500).send({success: false, message: "运单创建失败"}) } }) ); shippoRouter.get ( '/shippingInfo', expressAsyncHandler(async (req, res) => { const userId = req.user._id; // 从持久化存储取对应的shippingId const user = await User.findById(userId); if (!user?.shippingId) { return res.status(400).send({success: false, message: "未找到对应运单"}) } // 如果创建运单时已经存了rates,这里可以直接查库返回,不用调Shippo接口 const shipmentInfo = await shippoClient.shipment.retrieve(user.shippingId); res.send({ success: true, rates: shipmentInfo.rates }) }) ); export default shippoRouter;
内容的提问来源于stack exchange,提问作者Gigi101
相关产品推荐
相关产品推荐

