| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667 |
- <?php
- namespace common\components;
- use hd\models\IntraCity\StoreFeeForm;
- use Yii;
- use yii\helpers\Json;
- //use linslin\yii2\curl;
- use bizHd\wx\classes\WxOpenClass;
- use phpseclib\Crypt\RSA;
- class IntraCityExpress
- {
- /**
- * 订单状态常量
- */
- const ORDER_STATUS_CREATED = 10000; // 订单创建成功
- const ORDER_STATUS_CANCELED_BY_MERCHANT = 20000; // 商家取消订单
- const ORDER_STATUS_CANCELED_BY_DELIVERY = 20001; // 配送方取消订单
- const ORDER_STATUS_ACCEPTED = 30000; // 配送员接单
- const ORDER_STATUS_ARRIVED = 40000; // 配送员到店
- const ORDER_STATUS_DELIVERING = 50000; // 配送中
- const ORDER_STATUS_WITHDRAWN = 60000; // 配送员撤单
- const ORDER_STATUS_COMPLETED = 70000; // 配送完成
- const ORDER_STATUS_EXCEPTION = 90000; // 配送异常
- // 物品类型
- const GOODS_TYPE_FLOWER = 14; // 鲜花
- /**
- * 获取 access_token
- * @param $merchant
- * @param int $ptStyle
- * @return string
- */
- public static function getAccessToken($merchant, $ptStyle = 0)
- {
- if ($ptStyle == 0) {
- $ptStyle = dict::getDict('ptStyle', 'hd');
- }
- // 直接复用 miniUtil 的获取小程序 access_token 方法
- return miniUtil::getMiniProgramAccessToken($merchant, $ptStyle);
- }
- public static function getMerchant()
- {
- $merchant = WxOpenClass::getWxInfo();
- return $merchant;
- }
- /**
- * 使用 WxApi 方式生成微信API签名
- * @param string $url 请求URL
- * @param string $appId 小程序AppID
- * @param int $timestamp 时间戳
- * @param string $reqData 请求体JSON字符串
- * @return string 签名字符串
- */
- private static function generateWxApiSignature($url, $appId, $timestamp, $reqData)
- {
- // 获取私钥
- $env = getenv('YII_ENV') == 'production' ? 'production' : 'dev';
- $privateKeyPath = __DIR__ . '/wxapi/' . $env . '/rsa-private-key.txt';
- if (!file_exists($privateKeyPath)) {
- throw new \Exception("私钥文件不存在: {$privateKeyPath}");
- }
- $privateKey = file_get_contents($privateKeyPath);
- if (!$privateKey) {
- throw new \Exception('无法获取私钥文件');
- }
- // 构建签名载荷,格式:$url\n$appId\n$timestamp\n$reqData
- $payload = "$url\n$appId\n$timestamp\n$reqData";
- // 方案一: 使用 OpenSSL(推荐方式,与微信官方示例一致)
- // $signature = '';
- // $result = openssl_sign($payload, $signature, $privateKey, OPENSSL_ALGO_SHA256);
- // if (!$result) {
- // throw new \Exception('OpenSSL签名失败: ' . openssl_error_string());
- // }
- // $base64Signature = base64_encode($signature);
- // \Yii::info("生成的签名: " . $base64Signature, 'intracity_signature');
- // return $base64Signature;
- // 方案二:使用 RSA SHA256签名
- try {
- // 使用RSA SHA256签名
- $rsa = new RSA();
- $rsa->loadKey($privateKey);
- $rsa->setHash("sha256");
- $rsa->setMGFHash("sha256");
- $signature = $rsa->sign($payload);
- // $rsa = new RSA();
- // $rsa->loadKey($privateKey);
- // $rsa->setSignatureMode(RSA::SIGNATURE_PKCS1); // 明确使用PKCS#1 v1.5
- // $rsa->setHash("sha256");
- // 不要设置MGF,PKCS#1 v1.5不使用MGF
- // $signature = $rsa->sign($payload);
- $base64Signature = base64_encode($signature);
- \Yii::info("phpseclib生成的签名: " . $base64Signature, 'intracity_signature');
- return $base64Signature;
- } catch (\Exception $e) {
- \Yii::error("所有签名方法都失败 - OpenSSL: " . $e->getMessage() . " phpseclib: " . $e->getMessage(), 'intracity_signature');
- throw new \Exception('签名生成失败: OpenSSL和phpseclib都失败');
- }
- }
- /**
- * 发送HTTP请求
- * @param string $url 请求URL
- * @param array $data 请求数据
- * @param string $accessToken access_token
- * @return array
- */
- public static function sendRequest($url, $data, $accessToken)
- {
- // ================ 选择哪个 appId =================
- // 微信开放平台 appId
- $open = \biz\wx\classes\WxOpenClass::getByCondition(['style' => dict::getDict('ptStyle', 'hd')]);
- if (empty($open)) {
- util::fail('没有找到平台信息');
- }
- $component_app_id = $open['appId'];
- $appId = $component_app_id;
- // 当前小程序的 appId
- //$appId = $merchant['miniAppId'] ?? util::fail('app-id 出错');
- // 方式一: 使用WxApi方式处理请求 ============================
- try {
- $wxApi = new \common\components\wxapi\WxApi($appId, $accessToken);
- $result = $wxApi->request($url, $data);
- return $result;
- } catch (\Exception $e) {
- Yii::error($e->getMessage());
- $msg = $e->getMessage();
- // 如果WxApi方式失败,回退到简单方式
- util::fail($msg);
- }
- // 添加access_token到URL
- $urlWithToken = $url . (strpos($url, '?') !== false ? '&' : '?') . 'access_token=' . $accessToken;
- $timestamp = time();
- $body = Json::encode($data);
- if ($body === '[]') {
- $body = '{}';
- }
- // 方式二: 自己处理请求 =========== 失败,不使用(可删除)
- // $signature = self::generateWxApiSignature($url, $appId, $timestamp, $body);
- // 构建请求头,包含微信API签名所需的头部
- // $headers = [
- // 'Content-Type: application/json',
- // 'Accept: application/json',
- // 'Wechatmp-Appid: ' . $appId,
- // 'Wechatmp-TimeStamp: ' . $timestamp,
- // 'Wechatmp-Signature: ' . $signature
- // ];
- // $curl = new curl\Curl();
- // $response = $curl->setOption(CURLOPT_POSTFIELDS, $body)
- // ->setOption(CURLOPT_HTTPHEADER, $headers)
- // ->post($urlWithToken);
- // if (is_array($response)) {
- // return $response;
- // }
- // return Json::decode($response, true);
- }
- // ==================== API URL常量 ====================
- const API_BASE_URL = 'https://api.weixin.qq.com/cgi-bin/express/intracity';
- // 门店管理
- const API_CREATE_STORE = self::API_BASE_URL . '/createstore';
- const API_UPDATE_STORE = self::API_BASE_URL . '/updatestore';
- const API_QUERY_STORE = self::API_BASE_URL . '/querystore';
- // 开通门店权限
- const API_APPLY_STORE = self::API_BASE_URL . '/apply';
- // 订单管理
- const API_ADD_ORDER = self::API_BASE_URL . '/addorder';
- const API_CANCEL_ORDER = self::API_BASE_URL . '/cancelorder';
- const API_GET_ORDER = self::API_BASE_URL . '/queryorder';
- // 预下单
- const API_PRE_ADD_ORDER = self::API_BASE_URL . '/preaddorder';
- // 资金管理
- const API_BALANCE_QUERY = self::API_BASE_URL . '/balancequery';
- const API_STORE_CHARGE = self::API_BASE_URL . '/storecharge';
- const API_STORE_REFUND = self::API_BASE_URL . '/storerefund';
- const API_CHARGE_RECORD = self::API_BASE_URL . '/queryflow';
- // 测试接口
- const API_MOCK_NOTIFY = self::API_BASE_URL . '/mocknotify';
- // ==================== 门店管理接口 ====================
- /**
- * 创建门店
- * @param array $storeData 门店信息
- * @param string $merchant 商户信息
- * @param int $ptStyle
- * @return array
- */
- public static function createStore($storeData, $merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- $data = [
- 'out_store_id' => $storeData['out_store_id'],
- 'store_name' => $storeData['store_name'],
- 'order_pattern' => 1,
- "address_info" => $storeData['address_info']
- ];
- return self::sendRequest(self::API_CREATE_STORE, $data, $accessToken);
- }
- /**
- * 更新门店
- * @param array $storeData 门店信息
- * @param string $merchant 商户信息
- * @param int $ptStyle
- * @return array
- */
- public static function updateStore($storeData, $merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- $data = $storeData;
- return self::sendRequest(self::API_UPDATE_STORE, $data, $accessToken);
- }
- /**
- * 查询门店
- * @param string $outStoreId 门店ID
- * @param string $merchant 商户信息
- * @param int $ptStyle
- * @return array
- */
- public static function getStore($outStoreId, $merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- $data = [
- 'out_store_id' => $outStoreId,
- ];
- return self::sendRequest(self::API_QUERY_STORE, $data, $accessToken);
- }
- /**
- * 开通门店权限
- * @param null $merchant
- * @param int $ptStyle
- * @return array
- */
- public static function applyStore($merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- return self::sendRequest(self::API_APPLY_STORE, [], $accessToken);
- }
- // ==================== 订单管理接口 ====================
- /**
- * 创建订单
- * @param array $orderData 订单信息
- * @param string $merchant 商户信息
- * @param int $ptStyle
- * @return array
- */
- public static function createOrder($orderData, $merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- //开发环境只能使用沙箱 ssh
- if (getenv('YII_ENV') != 'production') {
- $orderData['use_sandbox'] = 1;
- $orderData['user_name'] = '顺丰同城';
- $orderData['user_phone'] = (string)13881979410;
- $orderData['user_address'] = '北京市海淀区学清嘉创大厦A座15层';
- $orderData['user_lng'] = (double)116.352954;
- $orderData['user_lat'] = (double)40.014747;
- }
- $data = $orderData;
- return self::sendRequest(self::API_ADD_ORDER, $data, $accessToken);
- }
- /**
- * 预下单(查询运费)
- * @param array $orderData
- * @param null $merchant
- * @param int $ptStyle
- * @return array
- */
- public static function preAddOrder($orderData, $merchant = null, $ptStyle = 0)
- {
- // 花好鲜鲜花批发(安海部) user_name会被阻挡,不要打开
- // // 创建表单验证模型
- // $form = new StoreFeeForm();
- // $form->setScenario('query_fee');
- // $form->load($orderData, ''); // 直接从 post 数据加载,不使用模型名作为前缀
- // // 执行表单验证
- // $form->validateForm();
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- //开发环境只能使用沙箱 ssh
- if (getenv('YII_ENV') != 'production') {
- $orderData['use_sandbox'] = 1;
- $orderData['user_name'] = '顺丰同城';
- $orderData['user_phone'] = (string)13881979410;
- $orderData['user_address'] = '北京市海淀区学清嘉创大厦A座15层';
- $orderData['user_lng'] = (double)116.352954;
- $orderData['user_lat'] = (double)40.014747;
- }
- // 请求体
- $data = $orderData;
- return self::sendRequest(self::API_PRE_ADD_ORDER, $data, $accessToken);
- }
- /**
- * 取消订单
- * @param string $wxOrderId 微信订单号
- * @param string $storeOrderId 商户订单号
- * @param string $wxStoreId 门店ID
- * @param string $merchant 商户信息
- * @param int $ptStyle
- * @return array
- */
- public static function cancelOrder($wxOrderId = '', $storeOrderId = '', $wxStoreId = '', $merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- $data = [];
- if (!empty($wxOrderId)) {
- $data['wx_order_id'] = $wxOrderId;
- }
- if (!empty($storeOrderId)) {
- $data['store_order_id'] = $storeOrderId;
- }
- if (!empty($wxStoreId)) {
- $data['wx_store_id'] = $wxStoreId;
- }
- $cancelReasons = []; // 1:不需要了 2:信息填错 3:无人接单 99:其他
- $data['cancel_reason_id'] = 1;
- return self::sendRequest(self::API_CANCEL_ORDER, $data, $accessToken);
- }
- /**
- * 查询订单
- * @param string $wxOrderId 微信订单号
- * @param string $outOrderId 商户订单号
- * @param string $outStoreId 门店ID
- * @param string $merchant 商户信息
- * @param int $ptStyle
- * @return array
- */
- public static function queryOrder($wxStoreId, $storeOrderId = '', $wxOrderId = '', $merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- if (empty($wxOrderId) && (empty($storeOrderId) || empty($wxStoreId))) {
- util::fail("参数不足:wx_order_id 或 (store_order_id + wx_store_id) 必须提供一组");
- }
- $data = [];
- if ($wxStoreId != '') {
- $data['wx_order_id'] = $wxOrderId;
- }
- $data['store_order_id'] = $storeOrderId;
- $data['wx_store_id'] = $wxStoreId;
- return self::sendRequest(self::API_GET_ORDER, $data, $accessToken);
- }
- // ==================== 资金管理接口 ====================
- /**
- * 查询门店余额
- * @param string $wxStoreId
- * @param null $merchant
- * @param int $ptStyle
- * @return array
- */
- public static function getBalance($wxStoreId, $merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- $data = ['wx_store_id' => $wxStoreId]; //'out_store_id' => $outStoreId,
- return self::sendRequest(self::API_BALANCE_QUERY, $data, $accessToken);
- }
- /**
- * 门店充值
- * @param string $wxStoreId
- * @param string $outChargeId
- * @param int $amount
- * @param null $merchant
- * @param int $ptStyle
- * @return array
- */
- public static function storeCharge($wxStoreId, $chargeId, $amount, $merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- $data = [
- 'wx_store_id' => $wxStoreId,
- 'service_trans_id' => $chargeId,
- 'amount' => $amount,
- ];
- return self::sendRequest(self::API_STORE_CHARGE, $data, $accessToken);
- }
- /**
- * 门店退款
- * @param string $wxStoreId
- * @param string $payMode
- * @param string $transId
- * @param null $merchant
- * @param int $ptStyle
- * @return array
- */
- public static function storeFund($wxStoreId, $payMode, $transId, $merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- $data = [
- 'wx_store_id' => $wxStoreId,
- 'pay_mode' => $payMode,
- 'service_trans_id' => $transId,
- ];
- return self::sendRequest(self::API_STORE_REFUND, $data, $accessToken);
- }
- /**
- * 查询充值记录
- * @param string $outChargeId
- * @param null $merchant
- * @param int $ptStyle
- * @return array
- */
- public static function getChargeRecord($outChargeId, $merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- $data = ['out_charge_id' => $outChargeId];
- return self::sendRequest(self::API_CHARGE_RECORD, $data, $accessToken);
- }
- // ==================== 测试接口 ====================
- /**
- * 模拟回调接口
- * @param string $wxOrderId 微信订单号
- * @param string $outStoreId 门店ID
- * @param string $outOrderId 商户订单号
- * @param int $orderStatus 订单状态
- * @param string $merchant 商户信息
- * @param int $ptStyle
- * @return array
- */
- public static function mockNotify($orderStatus, $wxOrderId = '', $outStoreId = '', $outOrderId = '', $merchant = null, $ptStyle = 0)
- {
- if ($merchant === null) {
- $merchant = self::getMerchant();
- }
- $accessToken = self::getAccessToken($merchant, $ptStyle);
- $data = [
- 'order_status' => $orderStatus,
- ];
- if (!empty($wxOrderId)) {
- $data['wx_order_id'] = $wxOrderId;
- }
- // 注意:文档中模拟回调使用的是 wx_store_id 和 store_order_id
- if (!empty($outStoreId) && !empty($outOrderId)) {
- $data['wx_store_id'] = $outStoreId;
- $data['store_order_id'] = $outOrderId;
- }
- return self::sendRequest(self::API_MOCK_NOTIFY, $data, $accessToken);
- }
- /**
- * 生成回调签名
- * @param array $params 回调参数
- * @param string $token 安全token
- * @return string
- */
- public static function generateCallbackSignature($params, $token)
- {
- // 1. 筛选出参与签名的字段
- $signParams = [];
- $fieldsToSign = ['appid', 'order_status', 'service_trans_id', 'status_change_time', 'store_order_id', 'timestamp', 'wx_order_id', 'wx_store_id'];
- foreach ($fieldsToSign as $field) {
- if (isset($params[$field])) {
- $signParams[$field] = $params[$field];
- }
- }
- // 2. 按字典序排序参数
- ksort($signParams);
- // 3. 拼接参数字符串
- $signStr = '';
- foreach ($signParams as $key => $value) {
- $signStr .= $key . '=' . $value . '&';
- }
- $signStr .= 'token=' . $token;
- // 4. 计算MD5并转为小写
- return strtolower(md5($signStr));
- }
- // ==================== 回调验证接口 ====================
- /**
- * 验证回调签名
- * @param array $params 回调参数
- * @param string $token 安全token
- * @return bool
- */
- public static function verifyCallback($params, $token)
- {
- if (!isset($params['sign'])) {
- return false;
- }
- $receivedSign = $params['sign'];
- unset($params['sign']);
- $calculatedSign = self::generateCallbackSignature($params, $token);
- return $receivedSign === $calculatedSign;
- }
- /**
- * 处理订单状态回调
- * @param array $callbackData 回调数据
- * @param string $token 安全token
- * @return array
- */
- public static function handleOrderCallback($callbackData, $token)
- {
- // 验证签名
- if (!self::verifyCallback($callbackData, $token)) {
- return [
- 'return_code' => 1,
- 'return_msg' => '签名验证失败'
- ];
- }
- // 处理订单状态变化
- $orderStatus = $callbackData['order_status'];
- $wxOrderId = $callbackData['wx_order_id'];
- $outOrderId = $callbackData['store_order_id'] ?? '';
- $outStoreId = $callbackData['wx_store_id'] ?? '';
- // 这里可以添加具体的业务逻辑处理
- // 例如:更新数据库中的订单状态、发送通知等
- // 伪代码示例:
- /*
- switch ($orderStatus) {
- case 10000: // 订单创建成功
- // 处理订单创建成功逻辑
- break;
- case 30000: // 配送员接单
- // 处理配送员接单逻辑
- break;
- case 40000: // 配送员到店
- // 处理配送员到店逻辑
- break;
- case 50000: // 配送中
- // 处理配送中逻辑
- break;
- case 70000: // 配送完成
- // 处理配送完成逻辑
- break;
- case 20000: // 商家取消订单
- case 20001: // 配送方取消订单
- case 60000: // 配送员撤单
- // 处理订单取消逻辑
- break;
- case 90000: // 配送异常
- // 处理配送异常逻辑
- break;
- }
- */
- return [
- 'return_code' => 0,
- 'return_msg' => 'OK'
- ];
- }
- }
|