支付提交
This commit is contained in:
@@ -9,7 +9,6 @@ Route::group('v1/auth', function () {
|
||||
Route::post('login', 'app\common\controller\PasswordLoginController@index'); // 账号密码登录
|
||||
Route::post('mobile-login', 'app\common\controller\Auth@mobileLogin'); // 手机号验证码登录
|
||||
Route::post('code', 'app\common\controller\Auth@SendCodeController'); // 发送验证码
|
||||
|
||||
// 需要JWT认证的接口
|
||||
Route::get('info', 'app\common\controller\Auth@info')->middleware(['jwt']); // 获取用户信息
|
||||
Route::post('refresh', 'app\common\controller\Auth@refresh')->middleware(['jwt']); // 刷新令牌
|
||||
@@ -22,4 +21,13 @@ Route::group('v1/', function () {
|
||||
})->middleware(['jwt']);
|
||||
|
||||
|
||||
Route::get('app/update', 'app\common\controller\Api@uploadApp');
|
||||
|
||||
Route::group('v1/pay', function () {
|
||||
Route::post('', 'app\cunkebao\controller\Pay@createOrder')->middleware(['jwt']);
|
||||
Route::post('notify', 'app\common\controller\Attachment@notify');
|
||||
})->middleware(['jwt']);
|
||||
|
||||
|
||||
|
||||
|
||||
Route::get('app/update', 'app\common\controller\PaymentService@createOrder');
|
||||
329
Server/application/common/controller/PaymentService.php
Normal file
329
Server/application/common/controller/PaymentService.php
Normal file
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use think\Db;
|
||||
use app\common\util\PaymentUtil;
|
||||
use think\facade\Config;
|
||||
use think\facade\Env;
|
||||
use think\facade\Log;
|
||||
use think\facade\Request;
|
||||
use app\common\model\Order;
|
||||
|
||||
/**
|
||||
* 支付服务(内部调用)
|
||||
*/
|
||||
class PaymentService
|
||||
{
|
||||
/**
|
||||
* 下单
|
||||
*
|
||||
* @param array $order
|
||||
* - out_trade_no: string 商户订单号(必填)
|
||||
* - total_fee: int 金额(分,必填)
|
||||
* - body: string 商品描述(必填)
|
||||
* - notify_url: string 异步通知地址(可覆盖配置)
|
||||
* - attach: string 附加数据(可选)
|
||||
* - time_expire: string 订单失效时间(可选)
|
||||
* - client_ip: string 终端IP(可选)
|
||||
* - sign_type: string MD5/RSA_1_256/RSA_1_1(可选,默认MD5)
|
||||
* - pay_type: string 支付场景,如 JSAPI/APP/H5(可选)
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createOrder(array $order)
|
||||
{
|
||||
|
||||
$params = [
|
||||
'service' => 'unified.trade.native',
|
||||
'sign_type' => PaymentUtil::SIGN_TYPE_MD5,
|
||||
'mch_id' => Env::get('payment.mchId'),
|
||||
'out_trade_no' => $order['orderNo'],
|
||||
'body' => $order['goodsName'] ?? '',
|
||||
'total_fee' => $order['money'] ?? 0,
|
||||
'mch_create_ip' => Request::ip(),
|
||||
'notify_url' => Env::get('payment.notify_url',''),
|
||||
'nonce_str' => PaymentUtil::generateNonceStr(),
|
||||
];
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 过滤空值签名
|
||||
$secret = Env::get('payment.key');
|
||||
$params['sign_type'] = 'MD5';
|
||||
$params['sign'] = PaymentUtil::generateSign($params, $secret, 'MD5');
|
||||
|
||||
$url = Env::get('payment.url');
|
||||
if (empty($url)) {
|
||||
throw new \Exception('支付网关地址未配置');
|
||||
}
|
||||
|
||||
//创建订单
|
||||
Order::create([
|
||||
'mchId' => $params['mch_id'],
|
||||
'companyId' => $order['companyId'],
|
||||
'userId' => $order['userId'],
|
||||
'orderType' => $order['orderType'] ?? 1,
|
||||
'status' => 0,
|
||||
'goodsId' => $order['goodsId'],
|
||||
'goodsName' => $order['goodsName'],
|
||||
'money' => $order['money'],
|
||||
'orderNo' => $order['orderNo'],
|
||||
'ip' => Request::ip(),
|
||||
'nonceStr' => $params['nonce_str'],
|
||||
'createTime' => time(),
|
||||
]);
|
||||
|
||||
|
||||
// XML POST 请求
|
||||
$xmlBody = $this->arrayToXml($params);
|
||||
$response = $this->postXml($url, $xmlBody);
|
||||
$parsed = $this->parseXmlOrRaw($response);
|
||||
|
||||
if ($parsed['status'] == 0 && $parsed['result_code'] == 0) {
|
||||
Db::commit();
|
||||
return json(['code' => 200, 'msg' => '订单创建成功','data' => $parsed['code_url']]);
|
||||
}else{
|
||||
Db::rollback();
|
||||
return json(['code' => 500, 'msg' => '订单创建失败:' . $parsed['err_msg']]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return json(['code' => 500, 'msg' => '订单创建失败:' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* POST 请求(x-www-form-urlencoded)
|
||||
*/
|
||||
protected function httpPost(string $url, array $params, array $headers = [])
|
||||
{
|
||||
if (!function_exists('requestCurl')) {
|
||||
throw new \RuntimeException('requestCurl 未定义');
|
||||
}
|
||||
return requestCurl($url, $params, 'POST', $headers, 'dataBuild');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析响应
|
||||
*/
|
||||
protected function parseResponse($response)
|
||||
{
|
||||
if ($response === '' || $response === null) {
|
||||
return '';
|
||||
}
|
||||
$decoded = json_decode($response, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $decoded;
|
||||
}
|
||||
if (strpos($response, '=') !== false && strpos($response, '&') !== false) {
|
||||
$arr = [];
|
||||
foreach (explode('&', $response) as $pair) {
|
||||
if ($pair === '') continue;
|
||||
$kv = explode('=', $pair, 2);
|
||||
$arr[$kv[0]] = $kv[1] ?? '';
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 XML 方式 POST(text/xml)
|
||||
*/
|
||||
protected function postXml(string $url, string $xml)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: text/xml; charset=UTF-8'
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
$res = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组转 XML(按 ASCII 升序,字符串走 CDATA)
|
||||
*/
|
||||
protected function arrayToXml(array $data): string
|
||||
{
|
||||
// 过滤空值
|
||||
$filtered = [];
|
||||
foreach ($data as $k => $v) {
|
||||
if ($v === '' || $v === null) continue;
|
||||
$filtered[$k] = $v;
|
||||
}
|
||||
ksort($filtered, SORT_STRING);
|
||||
|
||||
$xml = '<xml>';
|
||||
foreach ($filtered as $key => $value) {
|
||||
if (is_numeric($value)) {
|
||||
$xml .= "<{$key}>{$value}</{$key}>";
|
||||
} else {
|
||||
$xml .= "<{$key}><![CDATA[{$value}]]></{$key}>";
|
||||
}
|
||||
}
|
||||
$xml .= '</xml>';
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 XML 响应
|
||||
*/
|
||||
protected function parseXmlOrRaw($response)
|
||||
{
|
||||
if (!is_string($response) || $response === '') {
|
||||
return $response;
|
||||
}
|
||||
libxml_use_internal_errors(true);
|
||||
$xml = simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA);
|
||||
if ($xml !== false) {
|
||||
$json = json_encode($xml, JSON_UNESCAPED_UNICODE);
|
||||
return json_decode($json, true);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付结果异步通知
|
||||
* - 威富通回调为 XML;需校验签名与业务字段并更新订单
|
||||
* - 回应:成功回"success",失败回"fail"
|
||||
* @return void
|
||||
*/
|
||||
public function notify()
|
||||
{
|
||||
$rawBody = file_get_contents('php://input');
|
||||
Log::info('[SwiftPass][notify] raw: ' . $rawBody);
|
||||
|
||||
$payload = $this->parseXmlOrRaw($rawBody);
|
||||
if (!is_array($payload) || empty($payload)) {
|
||||
Log::error('[SwiftPass][notify] parse fail');
|
||||
echo 'fail';
|
||||
return;
|
||||
}
|
||||
|
||||
// 基础字段
|
||||
$status = (string)($payload['status'] ?? '');
|
||||
$resultCode = (string)($payload['result_code'] ?? '');
|
||||
$outTradeNo = (string)($payload['out_trade_no'] ?? '');
|
||||
$totalFee = (int)($payload['total_fee'] ?? 0);
|
||||
$mchIdNotify = (string)($payload['mch_id'] ?? '');
|
||||
$signInNotify = (string)($payload['sign'] ?? '');
|
||||
$signType = (string)($payload['sign_type'] ?? 'MD5');
|
||||
|
||||
if ($status !== '0' || $resultCode !== '0') {
|
||||
Log::warning('[SwiftPass][notify] business not success', $payload);
|
||||
echo 'fail';
|
||||
return;
|
||||
}
|
||||
|
||||
// 验签
|
||||
$secret = Env::get('payment.key');
|
||||
if (empty($secret)) {
|
||||
Log::error('[SwiftPass][notify] payment.key not configured');
|
||||
echo 'fail';
|
||||
return;
|
||||
}
|
||||
|
||||
$verifyData = $payload;
|
||||
unset($verifyData['sign']);
|
||||
$calcSign = PaymentUtil::generateSign($verifyData, $secret, $signType ?: 'MD5');
|
||||
if (strcasecmp($calcSign, $signInNotify) !== 0) {
|
||||
Log::error('[SwiftPass][notify] sign mismatch', [
|
||||
'calc' => $calcSign,
|
||||
'recv' => $signInNotify,
|
||||
]);
|
||||
echo 'fail';
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验商户号
|
||||
$mchIdConfig = Env::get('payment.mchId');
|
||||
if (!empty($mchIdConfig) && $mchIdConfig !== $mchIdNotify) {
|
||||
Log::error('[SwiftPass][notify] mch_id mismatch', [
|
||||
'config' => $mchIdConfig,
|
||||
'notify' => $mchIdNotify,
|
||||
]);
|
||||
echo 'fail';
|
||||
return;
|
||||
}
|
||||
|
||||
// 业务处理:更新订单
|
||||
Db::startTrans();
|
||||
try {
|
||||
/** @var Order|null $order */
|
||||
$order = Order::where('orderNo', $outTradeNo)->lock(true)->find();
|
||||
if (!$order) {
|
||||
Db::rollback();
|
||||
Log::error('[SwiftPass][notify] order not found', ['out_trade_no' => $outTradeNo]);
|
||||
echo 'fail';
|
||||
return;
|
||||
}
|
||||
|
||||
// 金额校验(单位:分)
|
||||
if ((int)$order['money'] !== $totalFee) {
|
||||
Db::rollback();
|
||||
Log::error('[SwiftPass][notify] amount mismatch', [
|
||||
'order_money' => (int)$order['money'],
|
||||
'notify_fee' => $totalFee,
|
||||
]);
|
||||
echo 'fail';
|
||||
return;
|
||||
}
|
||||
|
||||
// 幂等:已支付直接返回成功
|
||||
if ((int)$order['status'] === 1) {
|
||||
Db::commit();
|
||||
echo 'success';
|
||||
return;
|
||||
}
|
||||
|
||||
$transactionId = (string)($payload['transaction_id'] ?? '');
|
||||
$timeEndStr = (string)($payload['time_end'] ?? '');
|
||||
$paidAt = $this->parsePayTime($timeEndStr) ?: time();
|
||||
|
||||
$order->save([
|
||||
'status' => 1,
|
||||
'transactionId' => $transactionId,
|
||||
'payTime' => $paidAt,
|
||||
'updateTime' => time(),
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
echo 'success';
|
||||
return;
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
Log::error('[SwiftPass][notify] exception: ' . $e->getMessage());
|
||||
echo 'fail';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析威富通时间(yyyyMMddHHmmss)为时间戳
|
||||
*/
|
||||
protected function parsePayTime(string $timeEnd)
|
||||
{
|
||||
if ($timeEnd === '') {
|
||||
return 0;
|
||||
}
|
||||
// 期望格式:20250102153045
|
||||
if (preg_match('/^\\d{14}$/', $timeEnd) !== 1) {
|
||||
return 0;
|
||||
}
|
||||
$dt = \DateTime::createFromFormat('YmdHis', $timeEnd, new \DateTimeZone('Asia/Shanghai'));
|
||||
return $dt ? $dt->getTimestamp() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
20
Server/application/common/model/Order.php
Normal file
20
Server/application/common/model/Order.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use think\Model;
|
||||
use think\model\concern\SoftDelete;
|
||||
|
||||
|
||||
class Order extends Model
|
||||
{
|
||||
use SoftDelete;
|
||||
|
||||
|
||||
// 设置数据表名
|
||||
protected $name = 'order';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = true;
|
||||
protected $createTime = 'createTime';
|
||||
}
|
||||
255
Server/application/common/util/PaymentUtil.php
Normal file
255
Server/application/common/util/PaymentUtil.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\util;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 支付工具类
|
||||
* 用于处理第三方支付相关功能
|
||||
* 仅限内部调用
|
||||
*/
|
||||
class PaymentUtil
|
||||
{
|
||||
/**
|
||||
* 签名算法类型
|
||||
*/
|
||||
const SIGN_TYPE_MD5 = 'MD5';
|
||||
const SIGN_TYPE_RSA_1_256 = 'RSA_1_256';
|
||||
const SIGN_TYPE_RSA_1_1 = 'RSA_1_1';
|
||||
|
||||
/**
|
||||
* 生成支付签名
|
||||
*
|
||||
* @param array $params 待签名参数
|
||||
* @param string $secretKey 签名密钥
|
||||
* @param string $signType 签名类型 MD5/RSA_1_256/RSA_1_1
|
||||
* @return string 签名结果
|
||||
*/
|
||||
public static function generateSign(array $params, string $secretKey, string $signType = self::SIGN_TYPE_MD5): string
|
||||
{
|
||||
// 1. 移除sign字段
|
||||
unset($params['sign']);
|
||||
|
||||
// 2. 过滤空值
|
||||
$params = array_filter($params, function($value) {
|
||||
return $value !== '' && $value !== null;
|
||||
});
|
||||
|
||||
// 3. 按字段名ASCII码从小到大排序
|
||||
ksort($params);
|
||||
|
||||
// 4. 拼接成QueryString格式
|
||||
$queryString = self::buildQueryString($params);
|
||||
|
||||
// 5. 根据签名类型生成签名
|
||||
switch (strtoupper($signType)) {
|
||||
case self::SIGN_TYPE_MD5:
|
||||
return self::generateMd5Sign($queryString, $secretKey);
|
||||
case self::SIGN_TYPE_RSA_1_256:
|
||||
return self::generateRsa256Sign($queryString, $secretKey);
|
||||
case self::SIGN_TYPE_RSA_1_1:
|
||||
return self::generateRsa1Sign($queryString, $secretKey);
|
||||
default:
|
||||
throw new \InvalidArgumentException('不支持的签名类型: ' . $signType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证支付签名
|
||||
*
|
||||
* @param array $params 待验证参数(包含sign字段)
|
||||
* @param string $secretKey 签名密钥
|
||||
* @param string $signType 签名类型
|
||||
* @return bool 验证结果
|
||||
*/
|
||||
public static function verifySign(array $params, string $secretKey, string $signType = self::SIGN_TYPE_MD5): bool
|
||||
{
|
||||
if (!isset($params['sign'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$receivedSign = $params['sign'];
|
||||
$generatedSign = self::generateSign($params, $secretKey, $signType);
|
||||
|
||||
return $receivedSign === $generatedSign;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建QueryString
|
||||
*
|
||||
* @param array $params 参数数组
|
||||
* @return string QueryString
|
||||
*/
|
||||
private static function buildQueryString(array $params): string
|
||||
{
|
||||
$pairs = [];
|
||||
foreach ($params as $key => $value) {
|
||||
$pairs[] = $key . '=' . $value;
|
||||
}
|
||||
return implode('&', $pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MD5签名
|
||||
*
|
||||
* @param string $queryString 待签名字符串
|
||||
* @param string $secretKey 密钥
|
||||
* @return string MD5签名
|
||||
*/
|
||||
private static function generateMd5Sign(string $queryString, string $secretKey): string
|
||||
{
|
||||
$signString = $queryString . '&key=' . $secretKey;
|
||||
return strtoupper(md5($signString));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成RSA256签名
|
||||
*
|
||||
* @param string $queryString 待签名字符串
|
||||
* @param string $privateKey 私钥
|
||||
* @return string RSA256签名
|
||||
*/
|
||||
private static function generateRsa256Sign(string $queryString, string $privateKey): string
|
||||
{
|
||||
$privateKey = self::formatPrivateKey($privateKey);
|
||||
$key = openssl_pkey_get_private($privateKey);
|
||||
if (!$key) {
|
||||
throw new \Exception('RSA私钥格式错误');
|
||||
}
|
||||
|
||||
$signature = '';
|
||||
$result = openssl_sign($queryString, $signature, $key, OPENSSL_ALGO_SHA256);
|
||||
openssl_pkey_free($key);
|
||||
|
||||
if (!$result) {
|
||||
throw new \Exception('RSA256签名失败');
|
||||
}
|
||||
|
||||
return base64_encode($signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成RSA1签名
|
||||
*
|
||||
* @param string $queryString 待签名字符串
|
||||
* @param string $privateKey 私钥
|
||||
* @return string RSA1签名
|
||||
*/
|
||||
private static function generateRsa1Sign(string $queryString, string $privateKey): string
|
||||
{
|
||||
$privateKey = self::formatPrivateKey($privateKey);
|
||||
$key = openssl_pkey_get_private($privateKey);
|
||||
if (!$key) {
|
||||
throw new \Exception('RSA私钥格式错误');
|
||||
}
|
||||
|
||||
$signature = '';
|
||||
$result = openssl_sign($queryString, $signature, $key, OPENSSL_ALGO_SHA1);
|
||||
openssl_pkey_free($key);
|
||||
|
||||
if (!$result) {
|
||||
throw new \Exception('RSA1签名失败');
|
||||
}
|
||||
|
||||
return base64_encode($signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化私钥
|
||||
*
|
||||
* @param string $privateKey 原始私钥
|
||||
* @return string 格式化后的私钥
|
||||
*/
|
||||
private static function formatPrivateKey(string $privateKey): string
|
||||
{
|
||||
$privateKey = str_replace(['-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----', "\n", "\r"], '', $privateKey);
|
||||
$privateKey = chunk_split($privateKey, 64, "\n");
|
||||
return "-----BEGIN PRIVATE KEY-----\n" . $privateKey . "-----END PRIVATE KEY-----";
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化公钥
|
||||
*
|
||||
* @param string $publicKey 原始公钥
|
||||
* @return string 格式化后的公钥
|
||||
*/
|
||||
private static function formatPublicKey(string $publicKey): string
|
||||
{
|
||||
$publicKey = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\n", "\r"], '', $publicKey);
|
||||
$publicKey = chunk_split($publicKey, 64, "\n");
|
||||
return "-----BEGIN PUBLIC KEY-----\n" . $publicKey . "-----END PUBLIC KEY-----";
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证RSA签名
|
||||
*
|
||||
* @param string $queryString 原始字符串
|
||||
* @param string $signature 签名
|
||||
* @param string $publicKey 公钥
|
||||
* @param string $signType 签名类型
|
||||
* @return bool 验证结果
|
||||
*/
|
||||
public static function verifyRsaSign(string $queryString, string $signature, string $publicKey, string $signType = self::SIGN_TYPE_RSA_1_256): bool
|
||||
{
|
||||
$publicKey = self::formatPublicKey($publicKey);
|
||||
$key = openssl_pkey_get_public($publicKey);
|
||||
if (!$key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$algorithm = $signType === self::SIGN_TYPE_RSA_1_1 ? OPENSSL_ALGO_SHA1 : OPENSSL_ALGO_SHA256;
|
||||
$result = openssl_verify($queryString, base64_decode($signature), $key, $algorithm);
|
||||
openssl_pkey_free($key);
|
||||
|
||||
return $result === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*
|
||||
* @param int $length 长度
|
||||
* @return string 随机字符串
|
||||
*/
|
||||
public static function generateNonceStr(int $length = 32): string
|
||||
{
|
||||
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
$str = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$str .= $chars[mt_rand(0, strlen($chars) - 1)];
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成时间戳
|
||||
*
|
||||
* @return int 时间戳
|
||||
*/
|
||||
public static function generateTimestamp(): int
|
||||
{
|
||||
return time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化金额(分转元)
|
||||
*
|
||||
* @param int $amount 金额(分)
|
||||
* @return string 格式化后的金额(元)
|
||||
*/
|
||||
public static function formatAmount(int $amount): string
|
||||
{
|
||||
return number_format($amount / 100, 2, '.', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析金额(元转分)
|
||||
*
|
||||
* @param string $amount 金额(元)
|
||||
* @return int 金额(分)
|
||||
*/
|
||||
public static function parseAmount(string $amount): int
|
||||
{
|
||||
return (int) round(floatval($amount) * 100);
|
||||
}
|
||||
}
|
||||
135
Server/application/common/util/Signer.php
Normal file
135
Server/application/common/util/Signer.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\util;
|
||||
|
||||
/**
|
||||
* 第三方支付签名工具(仅内部调用)
|
||||
* 规则:
|
||||
* 1. 除 sign 外的所有非空参数,按字段名 ASCII 升序,使用 QueryString 形式拼接(key1=value1&key2=value2)
|
||||
* 2. 参与签名的字段名与值均为原始值,不做 URL Encode
|
||||
* 3. 支持算法:MD5(默认)/ RSA_1_256 / RSA_1_1
|
||||
*/
|
||||
class Signer
|
||||
{
|
||||
/**
|
||||
* 生成签名
|
||||
*
|
||||
* @param array $params 参与签名的参数(会自动剔除 sign 及空值)
|
||||
* @param string $algorithm 签名算法:md5 | RSA_1_256 | RSA_1_1
|
||||
* @param array $options 额外选项:
|
||||
* - secret: string MD5 签名时可选的密钥,若提供则会在原串末尾以 &key=SECRET 追加
|
||||
* - private_key: string RSA 签名所需私钥(PEM 字符串,支持带头尾)
|
||||
* - passphrase: string 可选,RSA 私钥口令
|
||||
* @return string 返回签名串(MD5 为32位小写;RSA为base64编码)
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function sign(array $params, $algorithm = 'md5', array $options = [])
|
||||
{
|
||||
$signString = self::buildSignString($params);
|
||||
|
||||
$algo = strtolower($algorithm);
|
||||
switch ($algo) {
|
||||
case 'md5':
|
||||
return self::signMd5($signString, isset($options['secret']) ? (string)$options['secret'] : null);
|
||||
case 'rsa_1_256':
|
||||
return self::signRsa($signString, $options, 'sha256');
|
||||
case 'rsa_1_1':
|
||||
return self::signRsa($signString, $options, 'sha1');
|
||||
default:
|
||||
throw new \InvalidArgumentException('Unsupported algorithm: ' . $algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建签名原始串
|
||||
* - 剔除 sign 字段
|
||||
* - 过滤空值(null、'')
|
||||
* - 按键名 ASCII 升序
|
||||
* - 使用原始值拼接为 key1=value1&key2=value2
|
||||
*
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
public static function buildSignString(array $params)
|
||||
{
|
||||
$filtered = [];
|
||||
foreach ($params as $key => $value) {
|
||||
if ($key === 'sign') {
|
||||
continue;
|
||||
}
|
||||
if ($value === '' || $value === null) {
|
||||
continue;
|
||||
}
|
||||
$filtered[$key] = $value;
|
||||
}
|
||||
|
||||
ksort($filtered, SORT_STRING);
|
||||
|
||||
$pairs = [];
|
||||
foreach ($filtered as $key => $value) {
|
||||
// 原始值拼接,不做 urlencode
|
||||
$pairs[] = $key . '=' . (is_bool($value) ? ($value ? '1' : '0') : (string)$value);
|
||||
}
|
||||
|
||||
return implode('&', $pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 签名
|
||||
* - 若提供 secret,则原串末尾追加 &key=SECRET
|
||||
* - 返回 32 位小写
|
||||
*
|
||||
* @param string $signString
|
||||
* @param string|null $secret
|
||||
* @return string
|
||||
*/
|
||||
protected static function signMd5($signString, $secret = null)
|
||||
{
|
||||
if ($secret !== null && $secret !== '') {
|
||||
$signString .= '&key=' . $secret;
|
||||
}
|
||||
return strtolower(md5($signString));
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA 签名
|
||||
*
|
||||
* @param string $signString
|
||||
* @param array $options 必填:private_key,可选:passphrase
|
||||
* @param string $hashAlgo sha256|sha1
|
||||
* @return string base64 签名
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected static function signRsa($signString, array $options, $hashAlgo = 'sha256')
|
||||
{
|
||||
if (empty($options['private_key'])) {
|
||||
throw new \InvalidArgumentException('RSA signing requires private_key.');
|
||||
}
|
||||
|
||||
$privateKey = $options['private_key'];
|
||||
$passphrase = isset($options['passphrase']) ? (string)$options['passphrase'] : '';
|
||||
|
||||
// 兼容无头尾私钥,自动包裹为 PEM
|
||||
if (strpos($privateKey, 'BEGIN') === false) {
|
||||
$privateKey = "-----BEGIN PRIVATE KEY-----\n" . trim(chunk_split(str_replace(["\r", "\n"], '', $privateKey), 64, "\n")) . "\n-----END PRIVATE KEY-----";
|
||||
}
|
||||
|
||||
$pkeyId = openssl_pkey_get_private($privateKey, $passphrase);
|
||||
if ($pkeyId === false) {
|
||||
throw new \InvalidArgumentException('Invalid RSA private key or passphrase.');
|
||||
}
|
||||
|
||||
$signature = '';
|
||||
$algoConst = $hashAlgo === 'sha1' ? OPENSSL_ALGO_SHA1 : OPENSSL_ALGO_SHA256;
|
||||
$ok = openssl_sign($signString, $signature, $pkeyId, $algoConst);
|
||||
openssl_free_key($pkeyId);
|
||||
|
||||
if (!$ok) {
|
||||
throw new \InvalidArgumentException('OpenSSL sign failed.');
|
||||
}
|
||||
|
||||
return base64_encode($signature);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
27
Server/application/cunkebao/controller/Pay.php
Normal file
27
Server/application/cunkebao/controller/Pay.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace app\cunkebao\controller;
|
||||
|
||||
|
||||
use app\common\controller\PaymentService;
|
||||
|
||||
class Pay
|
||||
{
|
||||
|
||||
public function createOrder()
|
||||
{
|
||||
$order = [
|
||||
'companyId' => 111,
|
||||
'userId' => 111,
|
||||
'orderNo' => time() . rand(100000, 999999),
|
||||
'goodsId' => 34,
|
||||
'goodsName' => '测试测试',
|
||||
'orderType' => 1,
|
||||
'money' => 1
|
||||
];
|
||||
|
||||
$paymentService = new PaymentService();
|
||||
$res = $paymentService->createOrder($order);
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
@@ -56,11 +56,11 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController
|
||||
}
|
||||
|
||||
if (!empty($device)) {
|
||||
$where[] = ['d.deviceId', '=', $device];
|
||||
// $where[] = ['d.deviceId', '=', $device];
|
||||
}
|
||||
|
||||
if (!empty($taskId)) {
|
||||
$where[] = ['t.sceneId', '=', $taskId];
|
||||
//$where[] = ['t.sceneId', '=', $taskId];
|
||||
}
|
||||
$where[] = ['s.companyId', '=', $this->getUserInfo('companyId')];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user