/** * @author: huxuejian * @description: 蜂鸟配送相关业务代码 * @doc https://open.ele.me/ * @date: 2025-12-02 */ const crypto = require('crypto'); const dayjs = require('dayjs'); const request = require('../../request'); const { getDatabase } = require("../../mongodb"); const baseUrl = process.env.CONFIG_FENGNIAO_API_DOMAIN; const appId = process.env.CONFIG_FENGNIAO_APPID; const appSecret = process.env.CONFIG_FENGNIAO_SECRET; const merchantId = process.env.CONFIG_FENGNIAO_MERCHANT_ID; const outShopCode = process.env.CONFIG_FENGNIAO_OUT_SHOP_CODE; const prefix = 'FengNiao' let dateBase = null; const tokenInfo = {}; const peddingToken = {}; const OrderStatus = { 0: '订单生成', 1: '运单生成成功', 20: '骑手接单', 80: '骑手到店', 2: '配送中', 3: '已完成', 4: '已取消(非蜂鸟侧取消)', 5: '配送异常(蜂鸟侧取消)', } const cancelErrorCode = { B0108: '运单状态不允许取消', B0110: '运单已取消', B0111: '运单不存在', B0113: '使用免责取消权益失败', A0100: '参数格式不正确', A0101: '缺少必填参数', "00001": '蜂鸟系统异常' } // const appSecret = '1111' /** * 安全解析 JSON,将超过安全整数范围的数字转换为字符串,避免精度丢失 * @param {string} jsonString - 要解析的 JSON 字符串 * @returns {any} 解析后的对象 */ function safeJsonParse(jsonString) { // 在解析前,使用正则表达式将超过安全整数范围的数字替换为字符串 // MAX_SAFE_INTEGER = 9007199254740991 (16位数字) // 匹配 ":数字" 格式,其中数字后面跟着 , } ] 或字符串结束 const maxSafeInteger = Number.MAX_SAFE_INTEGER; // 9007199254740991 // 匹配 JSON 中的数字值:":数字" 后面跟着 , } ] 或空白字符或字符串结束 // 匹配16位或更多位的整数(可能超过安全整数范围),包括负数 const bigIntRegex = /":\s*([+-]?\d{16,})([,}\]\s]|$)/g; const processedString = jsonString.replace(bigIntRegex, (match, numberStr, suffix) => { // 对于16位或更多位的数字,检查是否超过安全整数范围 const isNegative = numberStr.startsWith('-'); const absNumberStr = isNegative ? numberStr.substring(1) : numberStr; if (absNumberStr.length >= 16) { // 进一步检查:如果数字确实超过 MAX_SAFE_INTEGER,转换为字符串 // 使用字符串比较来避免精度丢失 if (absNumberStr.length > 16 || absNumberStr > String(maxSafeInteger)) { return `":"${numberStr}"${suffix}`; } } return match; }); return JSON.parse(processedString); } function getSignText(data) { if (Object.prototype.toString.call(data) !== '[object Object]') { throw new Error('获取签名文本失败: 参数类型错误') } const keys = Object.keys(data).sort(); const str = keys.map(key => `${key}=${data[key]}`).join('&'); const signBefore = `${appSecret}${str}`; // 使用SHA-256加密signBefore const messageDigest = crypto.createHash('sha256'); messageDigest.update(Buffer.from(signBefore, 'utf8')); const signText = messageDigest.digest('hex'); return { sortParam: str, signBefore, signText }; } async function getDb() { if (!dateBase) { dateBase = await getDatabase("Internet-hospital"); } if (!dateBase) { throw new Error("数据库连接失败"); } return dateBase; } async function getToken(corpId) { if (tokenInfo[corpId]) { return tokenInfo[corpId]; } const pendingToken = await getPendingToken(corpId); return pendingToken; } async function getPendingToken(corpId) { if (peddingToken[corpId]) { return await peddingToken[corpId]; } peddingToken[corpId] = getTokenFromDB(corpId); const token = await peddingToken[corpId]; tokenInfo[corpId] = token; peddingToken[corpId] = null; return token; } async function getTokenFromDB(corpId) { try { const db = await getDb(); const hlwConfig = require('../hlw-config'); const { data } = await hlwConfig({ type: 'getHlwConfig', corpId }, db); return data; } catch (e) { return null } } async function getValidToken() { if (!appId || !merchantId) { return { success: false, message: 'appId或merchant_id不能为空' }; } try { const corpId = `${prefix}_${appId}_${merchantId}`; const token = await getToken(corpId); if (!token) return { success: false, message: '[蜂鸟配送]token不存在,请联系工作人员重新授权' }; const { expireTime, refreshTime } = token; if (dayjs().isBefore(dayjs(expireTime))) { return { success: true, message: '[蜂鸟配送]token未过期', token }; } if (dayjs().isBefore(dayjs(refreshTime))) { peddingToken[`refresh_${corpId}`] = peddingToken[`refresh_${corpId}`] || refreshFengniaoToken(token) const newToken = await peddingToken[`refresh_${corpId}`]; peddingToken[`refresh_${corpId}`] = null; if (newToken.success) { return { success: true, message: '[蜂鸟配送]刷新token成功', token: newToken.data }; } return { success: false, message: newToken.message }; } return { success: false, message: '[蜂鸟配送]token已过期,请联系工作人员重新授权' }; } catch (e) { return { success: true, message: `[蜂鸟配送]获取token错误:${e.message || '未知错误'}` }; } } async function getFengniaoToken(code = '') { try { const db = await getDb(); const params = { app_id: appId, merchant_id: merchantId, grant_type: 'authorization_code', timestamp: `${Date.now()}`, } if (code) { params.code = code; } const { signText: signature } = getSignText(params); params.signature = signature; const url = `${baseUrl}/anubis-webapi/openapi/token`; const res = await request.main(url, params, 'POST'); if (res && res.code === '200') { const business_data = JSON.parse(res.business_data); const { app_id, // 应用id merchant_id,//商户id access_token, // 凭证token refresh_token, // 刷新token expire_in, //access_token剩余有效时间,单位:秒,默认有效期是一年 re_expire_in, //refresh_token剩余有效时间 } = business_data; const data = { app_id, merchant_id, access_token, refresh_token, expireTime: dayjs().add(expire_in, 'seconds').format('YYYY-MM-DD HH:mm:ss'), refreshTime: dayjs().add(re_expire_in, 'seconds').format('YYYY-MM-DD HH:mm:ss'), description: `蜂鸟配送token: 商户ID(${app_id})&门店ID(${merchant_id})`, } const corpId = `${prefix}_${app_id}_${merchant_id}`; tokenInfo[corpId] = data; const hlwConfig = require('../hlw-config'); await hlwConfig({ type: 'changeHlwConfig', corpId, data }, db); return { success: true, message: '[蜂鸟配送api]获取token成功', data }; } return { success: false, message: `[蜂鸟配送api]获取token失败:${res?.msg || '未知错误'}` }; } catch (e) { return { success: false, message: `[蜂鸟配送api]获取token错误:${e.message || '未知错误'}` }; } } async function refreshFengniaoToken(token) { try { const db = await getDb(); const params = { app_id: appId, merchant_id: merchantId, grant_type: 'refresh_token', timestamp: `${Date.now()}`, refresh_token: token.refresh_token, } const { signText: signature } = getSignText(params); params.signature = signature; const url = `${baseUrl}/anubis-webapi/openapi/refreshToken`; const res = await request.main(url, params, 'POST'); if (res && res.code === '200') { const business_data = JSON.parse(res.business_data); const { app_id, // 应用id merchant_id,//商户id access_token, // 凭证token refresh_token, // 刷新token expire_in, //access_token剩余有效时间,单位:秒,默认有效期是一年 re_expire_in, //refresh_token剩余有效时间 } = business_data; const data = { app_id, merchant_id, access_token, refresh_token, expireTime: dayjs().add(expire_in, 'seconds').format('YYYY-MM-DD HH:mm:ss'), refreshTime: dayjs().add(re_expire_in, 'seconds').format('YYYY-MM-DD HH:mm:ss'), } const corpId = `${prefix}_${app_id}_${merchant_id}`; tokenInfo[corpId] = data; const hlwConfig = require('../hlw-config'); await hlwConfig({ type: 'changeHlwConfig', corpId, data }, db); return { success: true, message: '[蜂鸟配送api]刷新token成功', data }; } return { success: false, message: `[蜂鸟配送api]刷新token失败:${res?.msg || '未知错误'}` }; } catch (e) { return { success: false, message: `[蜂鸟配送api]刷新token错误:${e.message || '未知错误'}` }; } } async function preCreateOrder(data) { try { const { success, message, token } = await getValidToken(); if (!success) return { success, message }; const businessData = { partner_order_code: data.partner_order_code, // 外部订单号 goods_count: data.goods_count,// 货物件数 receiver_latitude: data.receiver_latitude, // 收货人纬度 goods_total_amount_cent: data.goods_total_amount_cent,//订单商品总金额 单位:分 order_type: 1,//订单类型(1:即时单,3:预约单) // transport_longitude: data.transport_longitude,//取货点经度 // transport_latitude: data.transport_latitude,//取货点纬度 // transport_address: data.transport_address,//取货点地址 out_shop_code: data.out_shop_code,//外部门店id goods_weight: data.goods_weight, goods_item_list: data.goods_item_list.map(i => ({ item_actual_amount_cent: i.item_actual_amount_cent, // 商品总金额 (单价 x 数量) 单位:分 item_amount_cent: i.item_amount_cent, // 商品单价 单位:分 item_name: i.item_name, // 商品名称 item_quantity: i.item_quantity, // 商品数量 })), goods_actual_amount_cent: data.goods_actual_amount_cent, // 订单商品实际总金额 单位:分 receiver_longitude: data.receiver_longitude, // 收货人经度 receiver_address: data.receiver_address, // 收货人地址 position_source: 3, // 坐标经纬度来源(1:腾讯地图,2:百度地图, 3:高德地图),蜂鸟建议使用高德地图 } const params = { app_id: appId, merchant_id: merchantId, timestamp: Date.now(), version: '1.0', business_data: JSON.stringify(businessData), access_token: token.access_token, } const { signText: signature } = getSignText(params); params.signature = signature; const url = `${baseUrl}/anubis-webapi/v3/invoke/preCreateOrder`; const res = await request.main(url, params, 'POST'); if (res && res.code === '200') { const business_data = JSON.parse(res.business_data); const goods_infos = Array.isArray(business_data.goods_infos) ? business_data.goods_infos : []; const canDelivery = goods_infos.some(i => i && i.is_valid === 1); const result = { canDelivery, distance: typeof business_data.distance === 'number' ? business_data.distance : undefined, } return { success: true, message: '[蜂鸟配送api]预下单成功', data: result }; } return { success: false, message: `[蜂鸟配送api]预下单失败:${res?.msg || '未知错误'}` }; } catch (e) { return { success: false, message: `[蜂鸟配送api]预下单错误:${e.message || '未知错误'}` }; } } async function createOrder(data) { try { const { success, message, token } = await getValidToken(); if (!success) return { success, message }; const businessData = { partner_order_code: data.partner_order_code, // 外部订单号 receiver_primary_phone: data.receiver_primary_phone,// 收货人主要联系方式 goods_count: data.goods_count,// 货物件数 out_shop_code: data.out_shop_code, receiver_latitude: data.receiver_latitude, // 收货人纬度 goods_total_amount_cent: data.goods_total_amount_cent,//订单商品总金额 单位:分 receiver_name: data.receiver_name, order_type: 1,//订单类型(1:即时单,3:预约单) goods_weight: data.goods_weight, goods_item_list: data.goods_item_list.map(i => ({ item_actual_amount_cent: i.item_actual_amount_cent, // 商品总金额 (单价 x 数量) 单位:分 item_amount_cent: i.item_amount_cent, // 商品单价 单位:分 item_name: i.item_name, // 商品名称 item_quantity: i.item_quantity, // 商品数量 })), serial_number: data.serial_number, // 商家订单流水号, 方便配送骑手到店取货, 支持数字,字母及#等常见字符. 建议填写。长度<=8。 goods_actual_amount_cent: data.goods_actual_amount_cent, // 订单商品实际总金额 单位:分 receiver_longitude: data.receiver_longitude, // 收货人经度 receiver_address: data.receiver_address, // 收货人地址 position_source: 3 } const params = { app_id: appId, merchant_id: merchantId, timestamp: Date.now(), version: '1.0', business_data: JSON.stringify(businessData), access_token: token.access_token, } const { signText: signature } = getSignText(params); params.signature = signature; const url = `${baseUrl}/anubis-webapi/v3/invoke/createOrder`; const res = await request.main(url, params, 'POST'); if (res && res.code === '200') { const business_data = safeJsonParse(res.business_data); return { success: true, message: '[蜂鸟配送api]下单成功', shippingNo: business_data.order_id }; } return { success: false, message: `[蜂鸟配送api]下单失败:${res?.msg || '未知错误'}` }; } catch (e) { return { success: false, message: `[蜂鸟配送api]下单错误:${e.message || '未知错误'}` }; } } async function getOrderDetail({ orderNo: partner_order_code, shippingNo: order_id }) { try { const { success, message, token } = await getValidToken(); if (!success) return { success, message }; const businessData = { order_id, partner_order_code }; const params = { app_id: appId, merchant_id: merchantId, timestamp: Date.now(), version: '1.0', business_data: JSON.stringify(businessData), access_token: token.access_token, } const { signText: signature } = getSignText(params); params.signature = signature; const url = `${baseUrl}/anubis-webapi/v3/invoke/getOrderDetail`; const res = await request.main(url, params, 'POST'); if (res && res.code === '200') { const business_data = safeJsonParse(res.business_data); console.log(business_data) const result = { shippingNo: business_data.order_id, knightName: business_data.carrier_driver_name,//配送员姓名 knightPhone: business_data.carrier_driver_phone, arriveTime: business_data.estimate_arrive_time,//预计送达时间 partnerOrderCode: business_data.partner_order_code,//外部订单号 abnormalCode: business_data.abnormal_code,//异常编码 eventLogDetails: Array.isArray(business_data.event_log_details) ? business_data.event_log_details.map(i => ({ status: OrderStatus[i.order_status],//订单状态 orderStatus: i.order_status,//订单状态代码 time: i.occur_time && dayjs(i.occur_time).isValid() ? dayjs(i.occur_time).format('YYYY-MM-DD HH:mm:ss') : '',//事件时间 knightName: i.carrier_driver_name,//配送员姓名 knightPhone: i.carrier_driver_phone, })) : [], status: OrderStatus[business_data.order_status],//订单状态 photos: business_data.delivery_fetch_photos,//骑手取货照片地址列表 }; return { success: true, message: '[蜂鸟配送api]获取订单详情成功', data: result }; } if (res && res.msg === '订单不存在') { return { success: false, message: '[蜂鸟配送api]订单不存在', unexist: true }; } return { success: false, message: `[蜂鸟配送api]获取订单详情失败:${res?.msg || '未知错误'}` }; } catch (e) { return { success: false, message: `[蜂鸟配送api]获取订单详情错误:${e.message || '未知错误'}` }; } } async function cancelOrder(ctx) { try { const { orderNo: partner_order_code, role: order_cancel_role, // 1 商户 | 2 用户 cancelCode: order_cancel_code, reason: order_cancel_other_reason = '' } = ctx; const { success, message, token } = await getValidToken(); if (!success) return { success, message }; const businessData = { partner_order_code, order_cancel_role, order_cancel_code, order_cancel_other_reason }; const params = { app_id: appId, merchant_id: merchantId, timestamp: Date.now(), version: '1.0', business_data: JSON.stringify(businessData), access_token: token.access_token, } const { signText: signature } = getSignText(params); params.signature = signature; const url = `${baseUrl}/anubis-webapi/v3/invoke/cancelOrder`; const res = await request.main(url, params, 'POST'); if (res && res.code === '200') { const business_data = JSON.parse(res.business_data); if (business_data.result === true) { return { success: true, message: '[蜂鸟配送api]取消成功' }; } } const msg = cancelErrorCode[res?.err_code] || '未知错误'; return { success: false, message: `[蜂鸟配送api]取消失败:${msg}` }; } catch (e) { return { success: false, message: `[蜂鸟配送api]预取消错误:${e.message || '未知错误'}` }; } } async function preCancelOrder({ orderNo: partner_order_code, reasonCode: order_cancel_code }) { try { const { success, message, token } = await getValidToken(); if (!success) return { success, message }; const businessData = { order_cancel_code, partner_order_code }; const params = { app_id: appId, merchant_id: merchantId, timestamp: Date.now(), version: '1.0', business_data: JSON.stringify(businessData), access_token: token.access_token, } const { signText: signature } = getSignText(params); params.signature = signature; const url = `${baseUrl}/anubis-webapi/v3/invoke/preCancelOrder`; const res = await request.main(url, params, 'POST'); if (res && res.code === '200') { const business_data = safeJsonParse(res.business_data); const cancelCostCent = business_data.actual_cancel_cost_cent return { success: true, message: '[蜂鸟配送api]预取消成功', data: cancelCostCent }; } return { success: false, message: res?.msg || '未知错误' }; } catch (e) { return { success: false, message: e.message || '未知错误' }; } } async function getCancelReasonList({ orderNo: partner_order_code }) { try { if (!partner_order_code) { return { success: false, message: '外部订单号不能为空' }; } const { success, message, token } = await getValidToken(); if (!success) return { success, message }; const businessData = { partner_order_code }; const params = { app_id: appId, merchant_id: merchantId, timestamp: Date.now(), version: '1.0', business_data: JSON.stringify(businessData), access_token: token.access_token, } const { signText: signature } = getSignText(params); params.signature = signature; const url = `${baseUrl}/anubis-webapi/v3/invoke/getCancelReasonList`; const res = await request.main(url, params, 'POST'); if (res && res.code === '200') { const business_data = safeJsonParse(res.business_data); const cancelReasonList = Array.isArray(business_data.cancel_reason_list) ? business_data.cancel_reason_list.map(i => ({ code: i.order_cancel_code, name: i.order_cancel_desc })) : []; return { success: true, message: '[蜂鸟配送api]取消原因列表成功', data: cancelReasonList }; } return { success: false, message: res?.msg || '未知错误' }; } catch (e) { return { success: false, message: e.message || '未知错误' }; } } async function getStoreDeliveryRange() { try { const { success, message, token } = await getValidToken(); if (!success) return { success, message }; const businessData = { out_shop_code: outShopCode }; const params = { app_id: appId, merchant_id: merchantId, timestamp: Date.now(), version: '1.0', business_data: JSON.stringify(businessData), access_token: token.access_token, } const { signText: signature } = getSignText(params); params.signature = signature; const url = `${baseUrl}/anubis-webapi/v3/invoke/chainstoreRange`; const res = await request.main(url, params, 'POST'); if (res && res.code === '200') { const business_data = safeJsonParse(res.business_data); return { success: true, message: '[蜂鸟配送api]查询门店配送范围成功', data: business_data }; } return { success: false, message: `[蜂鸟配送api]查询门店配送范围失败:${res?.msg || '未知错误'}` }; } catch (e) { return { success: false, message: `[蜂鸟配送api查询门店配送范围错误:${e.message || '未知错误'}` }; } } function testPreOrder(ctx) { const address = typeof ctx.address === 'string' ? ctx.address.trim() : ''; const longitude = typeof ctx.longitude === 'number' ? ctx.longitude : 0; const latitude = typeof ctx.latitude === 'number' ? ctx.latitude : 0; if (!address || !latitude || !longitude) { return { success: false, message: '地址、经纬度不能为空' }; } const data = { partner_order_code: `FN${Date.now()}${Math.floor(Math.random() * 10000)}`, goods_count: 1, out_shop_code: outShopCode, receiver_latitude: latitude, goods_total_amount_cent: 1, goods_weight: 1, goods_item_list: [ { item_actual_amount_cent: 1, item_amount_cent: 1, item_name: '测试商品', item_quantity: 1 } ], goods_actual_amount_cent: 1, receiver_longitude: longitude, receiver_address: address, } return preCreateOrder(data); } exports.main = async (ctx) => { if (ctx.type === 'testFengniaoAvailable') { return await testPreOrder(ctx); } else if (ctx.type === 'createOrder') { return await createOrder(ctx.data); } else if (ctx.type === 'getOrderDetail') { return await getOrderDetail(ctx); } else if (ctx.type === 'getCancelReasons') { return await getCancelReasonList(ctx); } else if (ctx.type === 'cancelOrder') { return await cancelOrder(ctx); } else if (ctx.type === 'preCancelFengniaoOrder') { return await preCancelOrder(ctx); } return { success: false, message: '未找到接口' }; } exports.handleFengNiaoAuth = async (req, res) => { try { const code = typeof req.body?.code === 'string' ? req.body?.code.trim() : ''; const merchant_id = typeof req.body?.merchant_id === 'string' ? req.body?.merchant_id.trim() : ''; if (code === '') { return res.status(500).send("授权码不能为空"); } if (merchant_id === '') { return res.status(500).send("商户ID不能为空"); } const db = await getDb(); await db.collection('third_merchant_notify').insertOne( { createTime: new Date(), type: 'fengniaoAuthNotify', data: { code, merchant_id }, originData: JSON.stringify(req.body), } ) if (merchantId !== merchant_id) { return res.status(500).send("商户ID不匹配"); } const { success, message } = await getFengniaoToken(code); if (!success) { return res.status(500).send(message); } return res.status(200).send("success"); } catch (e) { res.status(500).send(e.message); } } exports.notify = async (req, res) => { try { const { app_id, timestamp, signature, business_data } = req.body; if (app_id !== appId || !timestamp || typeof signature !== 'string' || signature.trim() == '' || typeof business_data !== 'string' || business_data.trim() == '') { return res.status(500).send("缺少参数或参数错误"); } const db = await getDb(); await db.collection('third_merchant_notify').insertOne( { createTime: new Date(), type: 'fengniaoBusinessNotify', data: JSON.parse(business_data), originData: JSON.stringify(req.body), } ) return res.status(200).send("success"); } catch (e) { console.log('蜂鸟推送数据错误: ', e.message) res.status(500).send(e.message); } } exports.fengniaoTest = async (ctx) => { const api = ctx?.api; if (api === 'getReasons') { return await getCancelReasonList({ orderNo: ctx.orderNo }); } else if (api === 'preCancelOrder') { return await preCancelOrder({ orderNo: ctx.orderNo, reasonCode: ctx.reasonCode }); } else if (api === 'cancelOrder') { return await cancelOrder({ orderNo: ctx.orderNo, role: 1, cancelCode: ctx.cancelCode, reason: ctx.reason }); } else if (api === 'getStoreDeliveryRange') { return await getStoreDeliveryRange(); } }