643 lines
25 KiB
JavaScript
Raw Normal View History

2026-07-27 11:28:33 +08:00
/**
* @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();
}
}