初始化项目:添加后端代码、ThinkPHP框架、前端资源
This commit is contained in:
@@ -0,0 +1,850 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\library\pay;
|
||||
|
||||
use app\common\exception\Exception;
|
||||
use think\Cache;
|
||||
|
||||
/**
|
||||
* 微信小程序虚拟支付服务类
|
||||
* 文档地址: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment.html
|
||||
*/
|
||||
class VirtualPayService
|
||||
{
|
||||
/**
|
||||
* 虚拟支付配置
|
||||
* @var array
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* 小程序配置
|
||||
* @var array
|
||||
*/
|
||||
protected $miniappConfig;
|
||||
|
||||
/**
|
||||
* 微信API基础地址
|
||||
* @var string
|
||||
*/
|
||||
const API_BASE = 'https://api.weixin.qq.com';
|
||||
|
||||
/**
|
||||
* 构造函数,初始化虚拟支付配置
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = \app\common\model\config\System::getConfig('virtual_pay');
|
||||
$this->miniappConfig = \app\common\model\config\System::getConfig('wxMiniProgram');
|
||||
|
||||
if (!$this->config || !$this->miniappConfig) {
|
||||
throw new Exception('虚拟支付配置未初始化');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取access_token(使用稳定版API)
|
||||
* @param bool $forceRefresh 是否强制刷新
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getAccessToken($forceRefresh = false)
|
||||
{
|
||||
$cacheKey = 'virtual_pay_access_token:' . UNIACID;
|
||||
|
||||
if (!$forceRefresh) {
|
||||
$token = Cache::get($cacheKey);
|
||||
if ($token) {
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
$url = self::API_BASE . '/cgi-bin/stable_token';
|
||||
$params = [
|
||||
'grant_type' => 'client_credential',
|
||||
'appid' => $this->miniappConfig['app_id'],
|
||||
'secret' => $this->miniappConfig['secret'],
|
||||
];
|
||||
|
||||
$result = $this->httpPost($url, json_encode($params, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
if (isset($result['access_token'])) {
|
||||
Cache::set($cacheKey, $result['access_token'], $result['expires_in'] - 300);
|
||||
return $result['access_token'];
|
||||
}
|
||||
|
||||
throw new Exception('获取access_token失败: ' . ($result['errmsg'] ?? '未知错误'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询用户代币余额
|
||||
* @param string $openid 用户openid
|
||||
* @return array
|
||||
*/
|
||||
public function queryUserBalance($openid)
|
||||
{
|
||||
$uri = '/xpay/query_user_balance';
|
||||
$accessToken = $this->getAccessToken();
|
||||
$params = [
|
||||
'openid' => $openid,
|
||||
'env' => intval($this->config['env']),
|
||||
'user_ip' => request()->ip(),
|
||||
];
|
||||
$body = $this->encodeBody($params);
|
||||
$paySig = $this->generatePaySig($uri, $body);
|
||||
$signature = $this->getServerSignature($openid, $body);
|
||||
|
||||
$url = self::API_BASE . $uri;
|
||||
$url .= '?access_token=' . rawurlencode($accessToken) . '&pay_sig=' . rawurlencode($paySig) . '&signature=' . rawurlencode($signature);
|
||||
|
||||
$result = $this->httpPost($url, $body);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扣减代币(代币支付)
|
||||
* @param string $openid 用户openid
|
||||
* @param int $amount 扣减数量
|
||||
* @param string $orderId 商户订单号
|
||||
* @param array $payItem 支付项信息
|
||||
* @return array
|
||||
*/
|
||||
public function currencyPay($openid, $amount, $orderId, $payItem = [])
|
||||
{
|
||||
$uri = '/xpay/currency_pay';
|
||||
$accessToken = $this->getAccessToken();
|
||||
$params = [
|
||||
'openid' => $openid,
|
||||
'env' => intval($this->config['env']),
|
||||
'user_ip' => request()->ip(),
|
||||
'amount' => intval($amount),
|
||||
'order_id' => $orderId,
|
||||
'payitem' => json_encode($payItem, JSON_UNESCAPED_UNICODE),
|
||||
'remark' => '代币支付',
|
||||
];
|
||||
$body = $this->encodeBody($params);
|
||||
$paySig = $this->generatePaySig($uri, $body);
|
||||
$signature = $this->getServerSignature($openid, $body);
|
||||
|
||||
$url = self::API_BASE . $uri;
|
||||
$url .= '?access_token=' . rawurlencode($accessToken) . '&pay_sig=' . rawurlencode($paySig) . '&signature=' . rawurlencode($signature);
|
||||
|
||||
$result = $this->httpPost($url, $body);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代币支付退款
|
||||
* @param string $openid 用户openid
|
||||
* @param int $amount 退还数量
|
||||
* @param string $orderId 原订单号
|
||||
* @param string $refundId 退款单号
|
||||
* @return array
|
||||
*/
|
||||
public function cancelCurrencyPay($openid, $amount, $orderId, $refundId)
|
||||
{
|
||||
$uri = '/xpay/cancel_currency_pay';
|
||||
$accessToken = $this->getAccessToken();
|
||||
$params = [
|
||||
'openid' => $openid,
|
||||
'env' => intval($this->config['env']),
|
||||
'user_ip' => request()->ip(),
|
||||
'pay_order_id' => $orderId,
|
||||
'order_id' => $refundId,
|
||||
'amount' => intval($amount),
|
||||
];
|
||||
$body = $this->encodeBody($params);
|
||||
$paySig = $this->generatePaySig($uri, $body);
|
||||
$signature = $this->getServerSignature($openid, $body);
|
||||
|
||||
$url = self::API_BASE . $uri;
|
||||
$url .= '?access_token=' . rawurlencode($accessToken) . '&pay_sig=' . rawurlencode($paySig) . '&signature=' . rawurlencode($signature);
|
||||
|
||||
$result = $this->httpPost($url, $body);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单(现金订单)
|
||||
* @param string $openid 用户openid
|
||||
* @param string $outTradeNo 商户订单号
|
||||
* @param string $wxOrderId 微信订单号
|
||||
* @param string $refundOrderId 退款单号(查询退款单状态时使用)
|
||||
* @return array
|
||||
*/
|
||||
public function queryOrder($openid, $outTradeNo = '', $wxOrderId = '', $refundOrderId = '')
|
||||
{
|
||||
if (empty($outTradeNo) && empty($wxOrderId) && empty($refundOrderId)) {
|
||||
return ['errcode' => -1, 'errmsg' => '订单号不能为空'];
|
||||
}
|
||||
|
||||
$uri = '/xpay/query_order';
|
||||
$params = [
|
||||
'openid' => $openid,
|
||||
'env' => intval($this->config['env']),
|
||||
];
|
||||
if (!empty($refundOrderId)) {
|
||||
$params['order_id'] = $refundOrderId;
|
||||
} elseif (!empty($outTradeNo)) {
|
||||
$params['order_id'] = $outTradeNo;
|
||||
} else {
|
||||
$params['wx_order_id'] = $wxOrderId;
|
||||
}
|
||||
$body = $this->encodeBody($params);
|
||||
|
||||
$result = $this->callApiWithRetry(function ($accessToken) use ($uri, $body) {
|
||||
$paySig = $this->generatePaySig($uri, $body);
|
||||
$url = self::API_BASE . $uri . '?access_token=' . rawurlencode($accessToken) . '&pay_sig=' . rawurlencode($paySig);
|
||||
return $this->httpPost($url, $body);
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知发货完成(现金订单)
|
||||
* @param string $outTradeNo 商户订单号
|
||||
* @return array
|
||||
*/
|
||||
public function notifyProvideGoods($outTradeNo)
|
||||
{
|
||||
$uri = '/xpay/notify_provide_goods';
|
||||
$params = [
|
||||
'order_id' => $outTradeNo,
|
||||
'env' => intval($this->config['env']),
|
||||
];
|
||||
$body = $this->encodeBody($params);
|
||||
|
||||
$result = $this->callApiWithRetry(function ($accessToken) use ($uri, $body) {
|
||||
$paySig = $this->generatePaySig($uri, $body);
|
||||
$url = self::API_BASE . $uri . '?access_token=' . rawurlencode($accessToken) . '&pay_sig=' . rawurlencode($paySig);
|
||||
return $this->httpPost($url, $body);
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动订单退款任务(现金订单)
|
||||
* @param string $openid 下单用户openid
|
||||
* @param string $outTradeNo 商户订单号
|
||||
* @param string $refundOrderId 退款单号
|
||||
* @param int $leftFee 当前剩余可退金额(分)
|
||||
* @param int $refundFee 本次退款金额(分)
|
||||
* @param string $reason 退款原因枚举
|
||||
* @param string $reqFrom 退款来源枚举
|
||||
* @return array
|
||||
*/
|
||||
public function refundOrder($openid, $outTradeNo, $refundOrderId, $leftFee, $refundFee, $reason = '0', $reqFrom = '3')
|
||||
{
|
||||
$uri = '/xpay/refund_order';
|
||||
$params = [
|
||||
'openid' => $openid,
|
||||
'order_id' => $outTradeNo,
|
||||
'refund_order_id' => $refundOrderId,
|
||||
'left_fee' => intval($leftFee),
|
||||
'refund_fee' => intval($refundFee),
|
||||
'biz_meta' => $outTradeNo,
|
||||
'refund_reason' => strval($reason),
|
||||
'req_from' => strval($reqFrom),
|
||||
'env' => intval($this->config['env']),
|
||||
];
|
||||
$body = $this->encodeBody($params);
|
||||
|
||||
$result = $this->callApiWithRetry(function ($accessToken) use ($uri, $body) {
|
||||
$paySig = $this->generatePaySig($uri, $body);
|
||||
$url = self::API_BASE . $uri . '?access_token=' . rawurlencode($accessToken) . '&pay_sig=' . rawurlencode($paySig);
|
||||
return $this->httpPost($url, $body);
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询退款单状态(现金订单退款)
|
||||
* 退款状态枚举:5-已退款 7-退款失败 8-用户退款完成
|
||||
* @param string $openid 用户openid
|
||||
* @param string $refundOrderId 退款单号
|
||||
* @return array 包含 status 和 order 信息
|
||||
*/
|
||||
public function queryRefundOrder($openid, $refundOrderId)
|
||||
{
|
||||
$result = $this->queryOrder($openid, '', '', $refundOrderId);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询查询退款状态,直到退款完成或超时
|
||||
* @param string $openid 用户openid
|
||||
* @param string $refundOrderId 退款单号
|
||||
* @param int $maxRetries 最大重试次数
|
||||
* @param int $intervalMs 重试间隔(毫秒)
|
||||
* @return array ['success' => bool, 'status' => int, 'message' => string]
|
||||
*/
|
||||
public function pollRefundStatus($openid, $refundOrderId, $maxRetries = 6, $intervalMs = 3000)
|
||||
{
|
||||
for ($i = 0; $i < $maxRetries; $i++) {
|
||||
usleep($intervalMs * 1000);
|
||||
|
||||
$result = $this->queryOrder($openid, '', '', $refundOrderId);
|
||||
|
||||
\think\Log::info('[VirtualPay] 轮询退款状态 refund_order_id=' . $refundOrderId . ', 第' . ($i + 1) . '次, result=' . json_encode($result));
|
||||
|
||||
if (isset($result['errcode']) && $result['errcode'] !== 0) {
|
||||
if ($result['errcode'] === 268490011) {
|
||||
continue;
|
||||
}
|
||||
return [
|
||||
'success' => false,
|
||||
'status' => -1,
|
||||
'message' => '查询退款状态失败:' . ($result['errmsg'] ?? '未知错误'),
|
||||
];
|
||||
}
|
||||
|
||||
$status = isset($result['order']['status']) ? intval($result['order']['status']) : -1;
|
||||
|
||||
if ($status === 8) {
|
||||
return [
|
||||
'success' => true,
|
||||
'status' => $status,
|
||||
'message' => '退款完成',
|
||||
];
|
||||
}
|
||||
|
||||
if ($status === 7) {
|
||||
return [
|
||||
'success' => false,
|
||||
'status' => $status,
|
||||
'message' => '退款失败',
|
||||
];
|
||||
}
|
||||
|
||||
if ($status === 5) {
|
||||
return [
|
||||
'success' => true,
|
||||
'status' => $status,
|
||||
'message' => '订单已退款',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'status' => -1,
|
||||
'message' => '退款状态查询超时,退款可能仍在处理中',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询商家账户可提现余额
|
||||
* @return array
|
||||
*/
|
||||
public function queryBizBalance()
|
||||
{
|
||||
$uri = '/xpay/query_biz_balance';
|
||||
$accessToken = $this->getAccessToken();
|
||||
$params = [
|
||||
'env' => intval($this->config['env']),
|
||||
];
|
||||
$body = $this->encodeBody($params);
|
||||
$paySig = $this->generatePaySig($uri, $body);
|
||||
|
||||
$url = self::API_BASE . $uri;
|
||||
$url .= '?access_token=' . rawurlencode($accessToken) . '&pay_sig=' . rawurlencode($paySig);
|
||||
|
||||
$result = $this->httpPost($url, $body);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建提现单
|
||||
* @param string $withdrawId 提现单号(长度[8,32],只允许字母、数字、_、-)
|
||||
* @param string $amount 提现金额(单位元,例如 "0.01",不传则全额提现)
|
||||
* @return array
|
||||
*/
|
||||
public function createWithdrawOrder($withdrawId, $amount = '')
|
||||
{
|
||||
$uri = '/xpay/create_withdraw_order';
|
||||
$accessToken = $this->getAccessToken();
|
||||
$params = [
|
||||
'withdraw_no' => $withdrawId,
|
||||
'env' => intval($this->config['env']),
|
||||
];
|
||||
|
||||
if ($amount !== '') {
|
||||
$params['withdraw_amount'] = $amount;
|
||||
}
|
||||
$body = $this->encodeBody($params);
|
||||
$paySig = $this->generatePaySig($uri, $body);
|
||||
|
||||
$url = self::API_BASE . $uri;
|
||||
$url .= '?access_token=' . rawurlencode($accessToken) . '&pay_sig=' . rawurlencode($paySig);
|
||||
|
||||
$result = $this->httpPost($url, $body);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询提现单
|
||||
* @param string $withdrawId 提现单号
|
||||
* @return array
|
||||
*/
|
||||
public function queryWithdrawOrder($withdrawId)
|
||||
{
|
||||
$uri = '/xpay/query_withdraw_order';
|
||||
$accessToken = $this->getAccessToken();
|
||||
$params = [
|
||||
'withdraw_no' => $withdrawId,
|
||||
'env' => intval($this->config['env']),
|
||||
];
|
||||
$body = $this->encodeBody($params);
|
||||
$paySig = $this->generatePaySig($uri, $body);
|
||||
|
||||
$url = self::API_BASE . $uri;
|
||||
$url .= '?access_token=' . rawurlencode($accessToken) . '&pay_sig=' . rawurlencode($paySig);
|
||||
|
||||
$result = $this->httpPost($url, $body);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成提现单号
|
||||
* @return string
|
||||
*/
|
||||
public function generateWithdrawNo()
|
||||
{
|
||||
return 'WD' . date('YmdHis') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取虚拟支付客户端配置(供小程序端调用wx.requestVirtualPayment)
|
||||
* @param string $openid 用户openid
|
||||
* @param string $sessionKey 用户session_key
|
||||
* @param string $outTradeNo 商户订单号
|
||||
* @param int $amount 支付金额(分)
|
||||
* @param string $mode 支付模式 short_series_goods|short_series_coin
|
||||
* @param string $productId 道具ID(道具直购时必填)
|
||||
* @return array
|
||||
*/
|
||||
public function getClientPayConfig($openid, $sessionKey, $outTradeNo, $amount, $mode = 'short_series_coin', $productId = '')
|
||||
{
|
||||
$env = $this->getClientPayEnv();
|
||||
$signData = [
|
||||
'offerId' => $this->config['offer_id'],
|
||||
'buyQuantity' => $this->getBuyQuantity($amount, $mode),
|
||||
'env' => $env,
|
||||
'currencyType' => 'CNY',
|
||||
'outTradeNo' => $outTradeNo,
|
||||
'attach' => $outTradeNo,
|
||||
];
|
||||
|
||||
if ($mode === 'short_series_goods') {
|
||||
if (empty($productId)) {
|
||||
throw new Exception('道具直购模式缺少product_id');
|
||||
}
|
||||
$signData['productId'] = $productId;
|
||||
$signData['goodsPrice'] = $this->yuanToFen($amount);
|
||||
}
|
||||
|
||||
$signDataJson = $this->encodeClientSignData($signData);
|
||||
$paySig = $this->generatePaySig('requestVirtualPayment', $signDataJson, $env);
|
||||
|
||||
if (empty($sessionKey)) {
|
||||
throw new Exception('用户session_key不存在,请重新登录');
|
||||
}
|
||||
|
||||
$signature = $this->generateUserSignature($sessionKey, $signDataJson);
|
||||
|
||||
return [
|
||||
'signData' => $signDataJson,
|
||||
'paySig' => $paySig,
|
||||
'signature' => $signature,
|
||||
'mode' => $mode,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成支付签名(pay_sig)
|
||||
* @param string $uri 请求路径
|
||||
* @param string $body 实际请求体或signData字符串
|
||||
* @return string
|
||||
*/
|
||||
public function generatePaySig($uri, $body, $env = null)
|
||||
{
|
||||
$appKey = $this->getAppKey($env);
|
||||
$signStr = $uri . '&' . $body;
|
||||
|
||||
return hash_hmac('sha256', $signStr, $appKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前环境的AppKey。
|
||||
* 沙箱AppKey未配置时兼容使用正式AppKey,和后台配置提示保持一致。
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getAppKey($env = null)
|
||||
{
|
||||
$env = $env === null ? intval($this->config['env'] ?? 0) : intval($env);
|
||||
$appKey = $env === 0
|
||||
? ($this->config['app_key'] ?? '')
|
||||
: ($this->config['sandbox_app_key'] ?? '');
|
||||
|
||||
if ($env !== 0 && trim((string)$appKey) === '') {
|
||||
$appKey = $this->config['app_key'] ?? '';
|
||||
}
|
||||
|
||||
$appKey = trim((string)$appKey);
|
||||
if ($appKey === '') {
|
||||
throw new Exception('虚拟支付AppKey未配置');
|
||||
}
|
||||
|
||||
return $appKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端拉起虚拟支付环境。
|
||||
* iOS端由微信路由到Apple支付,而Apple支付不支持沙箱环境,只能使用现网环境。
|
||||
* @return int
|
||||
*/
|
||||
protected function getClientPayEnv()
|
||||
{
|
||||
if (\app\common\library\Platform::getDeviceEnd() === 'ios') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return intval($this->config['env'] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务端用户态签名(需要从数据库获取session_key)
|
||||
* @param string $openid 用户openid
|
||||
* @param string $body 实际请求体
|
||||
* @return string
|
||||
*/
|
||||
protected function getServerSignature($openid, $body)
|
||||
{
|
||||
$oauth = \app\common\model\user\Oauth::where([
|
||||
'openid' => $openid,
|
||||
'provider' => 'Wechat',
|
||||
'platform' => 'wxMiniProgram'
|
||||
])->find();
|
||||
|
||||
if (!$oauth || empty($oauth->session_key)) {
|
||||
throw new Exception('用户session_key不存在,请重新登录');
|
||||
}
|
||||
|
||||
return $this->generateUserSignature($oauth->session_key, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成用户态签名(signature)
|
||||
* @param string $sessionKey 用户session_key
|
||||
* @param string $body 实际请求体或signData字符串
|
||||
* @return string
|
||||
*/
|
||||
public function generateUserSignature($sessionKey, $body)
|
||||
{
|
||||
return hash_hmac('sha256', $body, $sessionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带自动重试的API调用(access_token失效时自动刷新重试)
|
||||
* @param callable $apiCall API调用闭包,接收 access_token 参数
|
||||
* @return array
|
||||
*/
|
||||
protected function callApiWithRetry($apiCall)
|
||||
{
|
||||
$accessToken = $this->getAccessToken();
|
||||
$result = $apiCall($accessToken);
|
||||
|
||||
if (isset($result['errcode']) && in_array($result['errcode'], [40001, 40014, 42001], true)) {
|
||||
\think\Log::warning('[VirtualPay] access_token失效,强制刷新重试 errcode=' . $result['errcode']);
|
||||
$accessToken = $this->getAccessToken(true);
|
||||
$result = $apiCall($accessToken);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用同一份JSON字符串参与签名和实际请求,避免序列化差异导致验签失败。
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
protected function encodeBody($params)
|
||||
{
|
||||
return json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编码wx.requestVirtualPayment使用的signData。
|
||||
* signData会逐字节参与paySig和signature计算,必须把同一个字符串原样传给前端。
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
protected function encodeClientSignData($params)
|
||||
{
|
||||
$items = [];
|
||||
foreach ($params as $key => $value) {
|
||||
$items[] = json_encode((string)$key, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
. ': '
|
||||
. json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
return '{' . implode(', ', $items) . '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取wx.requestVirtualPayment购买数量。
|
||||
* 代币模式下需要带上后台配置的兑换比例,否则微信侧仍会按旧比例展示金币数量。
|
||||
* @param mixed $amount
|
||||
* @param string $mode
|
||||
* @return int
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getBuyQuantity($amount, $mode)
|
||||
{
|
||||
if ($mode === 'short_series_goods') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$originalPrice = floatval($amount);
|
||||
if ($originalPrice < 1 && $originalPrice > 0) {
|
||||
$originalPrice = 1;
|
||||
}
|
||||
|
||||
$quantity = (int)round($originalPrice * self::getExchangeRate());
|
||||
if ($quantity <= 0) {
|
||||
throw new Exception('虚拟支付金额不合法');
|
||||
}
|
||||
|
||||
return $quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将系统订单金额(元)转换为微信接口要求的分。
|
||||
* @param mixed $amount
|
||||
* @return int
|
||||
*/
|
||||
protected function yuanToFen($amount)
|
||||
{
|
||||
return intval(round(floatval($amount) * 100));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
* @param int $length 长度
|
||||
* @return string
|
||||
*/
|
||||
protected function generateNonce($length = 32)
|
||||
{
|
||||
return \fast\Random::alnum($length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成商户订单号
|
||||
* @return string
|
||||
*/
|
||||
public function generateOutTradeNo()
|
||||
{
|
||||
return 'VP' . date('YmdHis') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP GET 请求
|
||||
* @param string $url 请求地址
|
||||
* @param array $query 查询参数
|
||||
* @return array
|
||||
*/
|
||||
protected function httpGet($url, $query = [])
|
||||
{
|
||||
if (!empty($query)) {
|
||||
$url .= (strpos($url, '?') !== false ? '&' : '?') . http_build_query($query);
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
return ['errcode' => -1, 'errmsg' => 'HTTP请求失败: ' . $error];
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP POST 请求
|
||||
* @param string $url 请求地址
|
||||
* @param string $body 请求体
|
||||
* @return array
|
||||
*/
|
||||
protected function httpPost($url, $body)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
return ['errcode' => -1, 'errmsg' => 'HTTP请求失败: ' . $error];
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前是否为虚拟支付商品
|
||||
* @param string $goodsType 商品类型
|
||||
* @return bool
|
||||
*/
|
||||
public static function isVirtualPayGoods($goodsType)
|
||||
{
|
||||
$config = \app\common\model\config\System::getConfig('virtual_pay');
|
||||
if (!$config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$virtualGoodsTypes = self::normalizeVirtualGoodsTypes($config['virtual_goods_types'] ?? []);
|
||||
|
||||
return in_array($goodsType, $virtualGoodsTypes, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前平台和商品是否应使用虚拟支付
|
||||
* @param string $goodsType 商品类型
|
||||
* @return int 1-使用虚拟支付 0-不使用
|
||||
*/
|
||||
public static function isUseVirtualPay($goodsType)
|
||||
{
|
||||
$config = \app\common\model\config\System::getConfig('virtual_pay');
|
||||
if (!$config || ($config['status'] ?? 'close') !== 'open') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (\app\common\library\Platform::getPlatform() !== 'wxMiniProgram') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$virtualGoodsTypes = self::normalizeVirtualGoodsTypes($config['virtual_goods_types'] ?? []);
|
||||
if (empty($virtualGoodsTypes)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$result = in_array($goodsType, $virtualGoodsTypes, true) ? 1 : 0;
|
||||
|
||||
\think\Log::info('[VirtualPay] isUseVirtualPay 判断 goods_type=' . $goodsType . ', virtual_goods_types=' . json_encode($virtualGoodsTypes) . ', result=' . $result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取人民币与代币兑换比例。
|
||||
* @return int
|
||||
*/
|
||||
public static function getExchangeRate()
|
||||
{
|
||||
$config = \app\common\model\config\System::getConfig('virtual_pay');
|
||||
$rate = isset($config['exchange_rate']) ? intval($config['exchange_rate']) : 1;
|
||||
|
||||
return in_array($rate, [1, 10, 100], true) ? $rate : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按虚拟支付兑换比例转换价格。
|
||||
* @param mixed $price
|
||||
* @return string
|
||||
*/
|
||||
public static function convertPrice($price)
|
||||
{
|
||||
$originalPrice = floatval($price);
|
||||
if ($originalPrice < 1 && $originalPrice > 0) {
|
||||
$originalPrice = 1;
|
||||
}
|
||||
return number_format($originalPrice * self::getExchangeRate(), 2, '.', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 按虚拟支付兑换比例批量转换数组中的价格字段。
|
||||
* @param array $data
|
||||
* @param array $fields
|
||||
* @return array
|
||||
*/
|
||||
public static function convertPriceFields($data, $fields = ['price', 'price_marking'])
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
if (isset($data[$field]) && $data[$field] !== '') {
|
||||
$data[$field] = self::convertPrice($data[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容历史配置中复选框值为数组、JSON字符串或逗号字符串的情况。
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
*/
|
||||
protected static function normalizeVirtualGoodsTypes($value)
|
||||
{
|
||||
if (is_string($value)) {
|
||||
$value = trim($value);
|
||||
$decoded = json_decode($value, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||
$value = $decoded;
|
||||
} elseif ($value === '') {
|
||||
$value = [];
|
||||
} else {
|
||||
$value = explode(',', $value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($value as $item) {
|
||||
$item = trim((string)$item);
|
||||
if ($item !== '') {
|
||||
$list[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($list));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取虚拟支付配置
|
||||
* @return array|false
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user