643 lines
25 KiB
JavaScript
643 lines
25 KiB
JavaScript
|
|
/**
|
|||
|
|
* @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();
|
|||
|
|
}
|
|||
|
|
}
|