Files
code-bang/backend/application/common/service/EasyWeChatService.php
T
2026-09-21 14:57:50 +08:00

247 lines
9.2 KiB
PHP

<?php
namespace app\common\service;
use EasyWeChat\MiniApp\Application as MiniAppApplication;
use EasyWeChat\Pay\Application as PayApplication;
use EasyWeChat\Pay\Message as PayMessage;
use think\Log;
/**
* 微信服务(EasyWeChat 6.x)
* 文档: https://easywechat.com/6.x/
* - 小程序: MiniApp\Application
* - 支付(含回调验签): Pay\Application (微信支付 v3)
*/
class EasyWeChatService
{
public $config;
/** @var MiniAppApplication 小程序实例 */
public $app;
/** @var PayApplication 微信支付 v3 实例 */
public $payApp;
public function __construct()
{
$this->config = config('common.wechat');
// 小程序
$this->app = new MiniAppApplication([
'app_id' => $this->config['app_id'],
'secret' => $this->config['secret'],
'http' => ['throw' => false, 'timeout' => 10.0],
]);
// 微信支付 v3
$this->payApp = new PayApplication([
'mch_id' => $this->config['mch_id'],
'secret_key' => $this->config['secret_key'], // APIv3 密钥
'v2_secret_key' => $this->config['v2_secret_key'], // 旧 v2 密钥(兼容 v2 模式回调)
'private_key' => $this->config['private_key'], // 商户证书私钥
'certificate' => $this->config['certificate'], // 商户API证书
'platform_certs' => $this->config['platform_certs'], // 微信支付公钥/平台证书
'http' => ['throw' => false, 'timeout' => 10.0],
]);
}
/**
* 小程序登录(code2session)
* @param string $code wx.login 获取的 code
* @return array {openid, session_key, unionid?}
*/
public function login($code)
{
$res = $this->app->getUtils()->codeToSession($code);
Log::info('login-res' . json_encode([$res], JSON_UNESCAPED_UNICODE));
return $res;
}
/**
* 解密开放数据(手机号等)
* @param $sessionKey code2session 返回的 session_key
* @param $iv
* @param $encryptedData
* @return array
*/
public function decryData($sessionKey, $iv, $encryptedData)
{
return $this->app->getUtils()->decryptSession($sessionKey, $iv, $encryptedData);
}
/**
* 统一下单(小程序 JSAPI 支付, v3)
* @param string $body 商品描述
* @param string $out_trade_no 商户订单号
* @param float $total_fee 金额(单位:元)
* @param string $openid 支付用户 openid
* @param string $notify_url 支付结果通知地址,留空使用配置默认
* @return array 含 prepay_id;失败时含 message 等错误信息
*/
public function unify($body, $out_trade_no, $total_fee, $openid, $notify_url = '')
{
$params = [
'appid' => $this->config['app_id'],
'mchid' => (string)$this->payApp->getMerchant()->getMerchantId(),
'description' => $body,
'out_trade_no' => $out_trade_no,
'notify_url' => $notify_url ?: $this->config['notify_url'],
'amount' => ['total' => (int)round($total_fee * 100), 'currency' => 'CNY'], // 转为分
'payer' => ['openid' => $openid],
];
Log::info('unify-params' . json_encode($params, JSON_UNESCAPED_UNICODE));
$response = $this->payApp->getClient()->postJson('v3/pay/transactions/jsapi', $params);
$result = $response->toArray(false);
Log::info('unify-res' . json_encode($result, JSON_UNESCAPED_UNICODE));
return $result;
}
/**
* 小程序调起支付参数(wx.requestPayment)
* @param string $prepayId
* @return array {timeStamp, nonceStr, package, signType, paySign}
*/
public function sdkConfig($prepayId)
{
$config = $this->payApp->getUtils()->buildMiniAppConfig($prepayId, $this->config['app_id']);
Log::info('buildMiniAppConfig-res' . json_encode($config, JSON_UNESCAPED_UNICODE));
return $config;
}
/**
* 发送小程序订阅消息
* @param $user_id
* @param $template_id
* @param array $msgData
*/
public function sendMsg($user_id, $template_id, array $msgData = [])
{
$data = SubscribeMessageService::packMsg($user_id, $template_id, $msgData);
$response = $this->app->getClient()->postJson('/cgi-bin/message/subscribe/send', $data);
$res = $response->toArray(false);
Log::info('发送订阅消息-res' . json_encode(['data' => $data, 'res' => $res], JSON_UNESCAPED_UNICODE));
}
/**
* 商家转账到零钱(v3, 原 v2 企业付款已停用)
* 需在商户平台开通「商家转账」产品权限
* @param string $order_sn 提现单号(字母或数字)
* @param string $openid
* @param string $name 收款用户姓名(商家转账不校验时可留空)
* @param float $amount 金额(单位:元)
* @param string $desc
* @return array {out_batch_no, batch_id, create_time};失败时含 message
*/
public function mchPay($order_sn, $openid, $name, $amount, $desc = '用户提现')
{
$params = [
'appid' => $this->config['app_id'],
'out_batch_no' => $order_sn,
'batch_name' => '用户提现',
'batch_remark' => $desc,
'total_amount' => (int)round($amount * 100), // 转为分
'total_num' => 1,
'transfer_detail_list' => [
[
'out_detail_no' => $order_sn . 'D1', // 明细单号(字母数字,保持唯一)
'transfer_amount' => (int)round($amount * 100),
'transfer_remark' => $desc,
'openid' => $openid,
],
],
];
Log::info('商家转账-params' . json_encode($params, JSON_UNESCAPED_UNICODE));
$response = $this->payApp->getClient()->postJson('v3/transfer/batches', $params);
$res = $response->toArray(false);
Log::info('商家转账-res' . json_encode($res, JSON_UNESCAPED_UNICODE));
return $res;
}
/**
* 查询商家转账批次状态
* @param string $apply_sn 商家转账单号(out_batch_no)
* @return array 转账批次状态及明细
*/
public function queryBalance($apply_sn)
{
$response = $this->payApp->getClient()->get("v3/transfer/batches/out-batch-no/{$apply_sn}", [
'query' => [
'need_query_detail' => 'true',
'offset' => 0,
'limit' => 20,
'detail_status' => 'ALL',
],
]);
$res = $response->toArray(false);
Log::info('商家转账查询-res' . json_encode(['apply_sn' => $apply_sn, 'res' => $res], JSON_UNESCAPED_UNICODE));
return $res;
}
/**
* 微信支付退款(v3)
* @param string $order_sn 商户订单号
* @param string $refund_no 商户退款单号
* @param float $actual_amount 原订单金额(单位:元)
* @param float $refund_amount 退款金额(单位:元)
* @param string $desc 退款原因
* @return array
*/
public function refund($order_sn, $refund_no, $actual_amount, $refund_amount, $desc = '出票失败退款')
{
$params = [
'out_trade_no' => $order_sn,
'out_refund_no' => $refund_no,
'reason' => $desc,
'amount' => [
'refund' => (int)bcmul($refund_amount, 100), // 转为分
'total' => (int)bcmul($actual_amount, 100),
'currency' => 'CNY',
],
];
Log::info('微信订单退款-params' . json_encode($params, JSON_UNESCAPED_UNICODE));
$response = $this->payApp->getClient()->postJson('v3/refund/domestic/refunds', $params);
$res = $response->toArray(false);
Log::info('微信订单退款-res' . json_encode(['param' => func_get_args(), 'res' => $res], JSON_UNESCAPED_UNICODE));
return $res;
}
/**
* 查询退款(v3)
* @param string $refund_no 商户退款单号
* @return array
*/
public function refundQuery($refund_no)
{
$response = $this->payApp->getClient()->get("v3/refund/domestic/refunds/{$refund_no}");
$res = $response->toArray(false);
Log::info('微信订单退款查询-res' . json_encode(['param' => func_get_args(), 'res' => $res], JSON_UNESCAPED_UNICODE));
return $res;
}
/**
* 处理微信支付 v3 回调(验签+解密),返回 PSR-7 响应
* @param \Closure $paidHandler function(PayMessage $message){ 业务处理;异常时抛出则返回失败 }
* @return \Psr\Http\Message\ResponseInterface
*/
public function handlePayNotify(\Closure $paidHandler)
{
$server = $this->payApp->getServer();
$server->handlePaid(function (PayMessage $message, \Closure $next) use ($paidHandler) {
// 回调签名验证(官方推荐,验签失败抛异常 -> serve 返回失败,微信将重试)
$this->payApp->getValidator()->validate($this->payApp->getRequest());
$paidHandler($message);
return $next($message);
});
return $server->serve();
}
}