Files
oil/server-tp6/app/common/library/wechat/WxPay.php
T
2026-09-01 22:15:50 +08:00

409 lines
14 KiB
PHP
Raw 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.
<?php
// +----------------------------------------------------------------------
// | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2023 https://www.yiovo.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
// +----------------------------------------------------------------------
// | Author: 萤火科技 <admin@yiovo.com>
// +----------------------------------------------------------------------
namespace app\common\library\wechat;
use app\common\model\wxapp\Setting as WxappSettingModel;
use app\common\enum\OrderType as OrderTypeEnum;
use app\common\enum\order\PayType as OrderPayTypeEnum;
use app\common\library\helper;
use cores\exception\BaseException;
/**
* 微信支付
* Class WxPay
* @package app\common\library\wechat
*/
class WxPay extends WxBase
{
// 微信支付配置
private $config;
// 当前商城ID
private $storeId;
// 订单模型
private $modelClass = [
OrderTypeEnum::ORDER => \app\api\service\order\PaySuccess::class,
OrderTypeEnum::RECHARGE => \app\api\service\recharge\PaySuccess::class,
];
/**
* 构造函数
* @param array $config
* @param int|null $storeId
*/
public function __construct(array $config = [], ?int $storeId = null)
{
parent::__construct();
$this->config = $config;
$this->storeId = $storeId;
if (!empty($this->config)) {
$this->setConfig($this->config['app_id'], $this->config['app_secret']);
}
}
/**
* 统一下单API
* @param $orderNo
* @param $openid
* @param $totalFee
* @param int $orderType 订单类型
* @return array
* @throws \cores\exception\BaseException
*/
public function unifiedorder($orderNo, $openid, $totalFee, int $orderType = OrderTypeEnum::ORDER): array
{
// 当前时间
$time = time();
// 生成随机字符串
$nonceStr = md5($time . $openid);
// API参数
$params = [
'appid' => $this->appId,
'attach' => helper::jsonEncode(['order_type' => $orderType]),
'body' => $orderNo,
'mch_id' => $this->config['mchid'],
'nonce_str' => $nonceStr,
'notify_url' => base_url() . 'notice.php', // 异步通知地址
'openid' => $openid,
'out_trade_no' => $orderNo,
'spbill_create_ip' => \request()->ip(),
'total_fee' => (int)helper::bcmul($totalFee, 100), // 价格:单位分
'trade_type' => 'JSAPI',
];
// 生成签名
$params['sign'] = $this->makeSign($params);
// 请求API
$url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
$result = $this->post($url, $this->toXml($params));
$prepay = $this->fromXml($result);
// 请求失败
if ($prepay['return_code'] === 'FAIL') {
$errMsg = "微信支付api:{$prepay['return_msg']}";
throwError($errMsg, null, ['errorCode' => 'WECHAT_PAY', 'isCreated' => true]);
}
if ($prepay['result_code'] === 'FAIL') {
$errMsg = "微信支付api:{$prepay['err_code_des']}";
throwError($errMsg, null, ['errorCode' => 'WECHAT_PAY', 'isCreated' => true]);
}
// 生成 nonce_str 供前端使用
$paySign = $this->makePaySign($params['nonce_str'], $prepay['prepay_id'], $time);
return [
'prepay_id' => $prepay['prepay_id'],
'nonceStr' => $nonceStr,
'timeStamp' => (string)$time,
'paySign' => $paySign
];
}
/**
* 支付成功异步通知
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function notify()
{
if (!$xml = file_get_contents('php://input')) {
$this->returnCode(false, 'Not found DATA');
}
// 将服务器返回的XML数据转化为数组
$data = $this->fromXml($xml);
// 记录日志
log_record($xml);
log_record($data);
// 实例化订单模型
$model = $this->getOrderModel($data['out_trade_no'], $data['attach']);
// 订单信息
$order = $model->getOrderInfo();
empty($order) && $this->returnCode(false, '订单不存在');
// 小程序配置信息
$wxConfig = WxappSettingModel::getWxappConfig($order['store_id']);
// 设置支付秘钥
$this->config['apikey'] = $wxConfig['apikey'];
// 保存微信服务器返回的签名sign
$dataSign = $data['sign'];
// sign不参与签名算法
unset($data['sign']);
// 生成签名
$sign = $this->makeSign($data);
// 判断签名是否正确 判断支付状态
if (
($sign !== $dataSign)
|| ($data['return_code'] !== 'SUCCESS')
|| ($data['result_code'] !== 'SUCCESS')
) {
$this->returnCode(false, '签名失败');
}
// 订单支付成功业务处理
$status = $model->onPaySuccess(OrderPayTypeEnum::WECHAT, $data);
if ($status == false) {
$this->returnCode(false, $model->getError());
}
// 返回状态
$this->returnCode(true, 'OK');
}
/**
* 申请退款API
* @param string $transactionId 微信支付交易流水号
* @param string $totalFee 订单总金额
* @param string $refundFee 退款金额
* @return bool
* @throws BaseException
* @throws \cores\exception\BaseException
*/
public function refund(string $transactionId, string $totalFee, string $refundFee): bool
{
// 当前时间
$time = time();
// 生成随机字符串
$nonceStr = md5($time . $transactionId . $totalFee . $refundFee);
// API参数
$params = [
'appid' => $this->appId,
'mch_id' => $this->config['mchid'],
'nonce_str' => $nonceStr,
'transaction_id' => $transactionId,
'out_refund_no' => $time,
'total_fee' => (int)helper::bcmul($totalFee,100),
'refund_fee' => (int)helper::bcmul($refundFee,100),
];
// 生成签名
$params['sign'] = $this->makeSign($params);
// 请求API
$url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
$result = $this->post($url, $this->toXml($params), true, $this->getCertPem());
// 请求失败
if (empty($result)) {
throwError('微信退款api请求失败');
}
// 格式化返回结果
$prepay = $this->fromXml($result);
// 记录日志
log_record(['name' => '微信退款API', [
'params' => $params,
'result' => $result,
'prepay' => $prepay
]]);
// 请求失败
if ($prepay['return_code'] === 'FAIL') {
throwError("return_msg: {$prepay['return_msg']}");
}
if ($prepay['result_code'] === 'FAIL') {
throwError("err_code_des: {$prepay['err_code_des']}");
}
return true;
}
/**
* 获取cert证书文件
* @return string[]
* @throws \cores\exception\BaseException
*/
private function getCertPem(): array
{
if (empty($this->config['cert_pem']) || empty($this->config['key_pem'])) {
throwError('请先到后台小程序设置填写微信支付证书文件');
}
// cert目录
$filePath = __DIR__ . "/cert/{$this->storeId}/";
return [
'certPem' => $filePath . 'cert.pem',
'keyPem' => $filePath . 'key.pem'
];
}
/**
* 实例化订单模型 (根据attach判断)
* @param $orderNo
* @param null $attach
* @return mixed
*/
private function getOrderModel($orderNo, $attach = null)
{
$attach = helper::jsonDecode($attach);
// 判断订单类型返回对应的订单模型
$model = $this->modelClass[$attach['order_type']];
return new $model($orderNo);
}
/**
* 返回状态给微信服务器
* @param boolean $returnCode
* @param string|null $msg
*/
private function returnCode(bool $returnCode = true, string $msg = null)
{
// 返回状态
$return = [
'return_code' => $returnCode ? 'SUCCESS' : 'FAIL',
'return_msg' => $msg ?: 'OK',
];
// 记录日志
log_record([
'name' => '返回微信支付状态',
'data' => $return
]);
die($this->toXml($return));
}
/**
* 生成paySign
* @param string $nonceStr
* @param string $prepayId
* @param int $timeStamp
* @return string
*/
private function makePaySign(string $nonceStr, string $prepayId, int $timeStamp): string
{
$data = [
'appId' => $this->appId,
'nonceStr' => $nonceStr,
'package' => 'prepay_id=' . $prepayId,
'signType' => 'MD5',
'timeStamp' => $timeStamp,
];
// 签名步骤一:按字典序排序参数
ksort($data);
$string = $this->toUrlParams($data);
// 签名步骤二:在string后加入KEY
$string = $string . '&key=' . $this->config['apikey'];
// 签名步骤三:MD5加密
$string = md5($string);
// 签名步骤四:所有字符转为大写
return strtoupper($string);
}
/**
* 将xml转为array
* @param string $xml
* @return mixed
*/
private function fromXml(string $xml)
{
// 禁止引用外部xml实体
libxml_disable_entity_loader(true);
return helper::jsonDecode(helper::jsonEncode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)));
}
/**
* 生成签名
* @param array $values
* @return string 本函数不覆盖sign成员变量,如要设置签名需要调用SetSign方法赋值
*/
private function makeSign(array $values): string
{
//签名步骤一:按字典序排序参数
ksort($values);
$string = $this->toUrlParams($values);
//签名步骤二:在string后加入KEY
$string = $string . '&key=' . $this->config['apikey'];
//签名步骤三:MD5加密
$string = md5($string);
//签名步骤四:所有字符转为大写
return strtoupper($string);
}
/**
* 格式化参数格式化成url参数
* @param array $values
* @return string
*/
private function toUrlParams(array $values): string
{
$buff = '';
foreach ($values as $k => $v) {
if ($k != 'sign' && $v != '' && !is_array($v)) {
$buff .= $k . '=' . $v . '&';
}
}
return trim($buff, '&');
}
/**
* 输出xml字符
* @param array $values
* @return bool|string
*/
private function toXml(array $values)
{
if (!is_array($values)
|| count($values) <= 0
) {
return false;
}
$xml = "<xml>";
foreach ($values as $key => $val) {
if (is_numeric($val)) {
$xml .= "<" . $key . ">" . $val . "</" . $key . ">";
} else {
$xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">";
}
}
$xml .= "</xml>";
return $xml;
}
/**
* 发货
* API文档地址:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html
* @param string $transaction_id 微信支付交易id
* @param string $desc 商品描述
* @param string $openid 用户openid
* @return mixed
* @throws BaseException
*/
public function uploadShipping(string $transaction_id, string $desc, string $openid)
{
// 微信接口url
$accessToken = $this->getAccessToken();
$url = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token={$accessToken}";
$dt = new \DateTime('now', new \DateTimeZone('Asia/Shanghai'));
$upload_time = $dt->format('Y-m-d\TH:i:s.vP');
// 构建请求
$data = [
'order_key' => [
'order_number_type' => 2,
'transaction_id' => $transaction_id,
],
'logistics_type' => 3,
'delivery_mode' => 1,
'shipping_list' => [
[
'item_desc' => $desc,
]
],
'upload_time' => $upload_time,
'payer' => [
'openid' => $openid
]
];
// 返回结果
$result = $this->post($url, helper::jsonEncode($data));
// 记录日志
log_record([
'name' => '微信支付自动发货',
'params' => $data,
'result' => !strpos($result, 'errcode') ? 'true' : $result
]);
$data = helper::jsonDecode($result);
if ($data['errcode'] == 0) {
return $result;
}
$error = '微信支付自动发货 ' . $data['errmsg'];
/*if ($data['errcode'] == 41030) {
$error = '小程序页面不存在,请先发布上线后再生成';
}*/
// throwError($error);
}
}