2026-07-27 11:28:33 +08:00

643 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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();
}
}