chore(addon): 添加线下支付及华为支付基本配置

This commit is contained in:
2025-12-01 11:36:51 +08:00
parent 3a7f510e19
commit e51f6c6544
40 changed files with 3238 additions and 12 deletions

View File

@@ -0,0 +1,33 @@
<?php
/**
* 华为支付自定义视图配置
*/
return [
// 自定义模板页面类型,格式:[ 'title' => '页面类型名称', 'name' => '页面标识', 'path' => '页面路径', 'value' => '页面数据json格式' ]
'template' => [],
// 后台自定义组件——装修
'util' => [],
// 自定义页面路径
'link' => [],
// 自定义图标库
'icon_library' => [],
// uni-app 组件,格式:[ 'name' => '组件名称/文件夹名称', 'path' => '文件路径/目录路径' ]多个逗号隔开自定义组件名称前缀必须是diy-,也可以引用第三方组件
'component' => [],
// uni-app 页面,多个逗号隔开
'pages' => [],
// 模板信息,格式:'title' => '模板名称', 'name' => '模板标识', 'cover' => '模板封面图', 'preview' => '模板预览图', 'desc' => '模板描述'
'info' => [],
// 主题风格配色格式可以自由定义扩展【在uni-app中通过this.themeStyle... 获取定义的颜色字段例如this.themeStyle.main_color】
'theme' => [],
// 自定义页面数据,格式:[ 'title' => '页面名称', 'name' => "页面标识", 'value' => [页面数据json格式] ]
'data' => []
];

View File

@@ -0,0 +1,45 @@
<?php
/**
* 华为支付扩展事件配置
*/
return [
'bind' => [
],
'listen' => [
//支付异步回调
'PayNotify' => [
'addon\huaweipay\event\PayNotify'
],
//支付方式,后台查询
'PayType' => [
'addon\huaweipay\event\PayType'
],
//支付,前台应用
'Pay' => [
'addon\huaweipay\event\Pay'
],
'PayClose' => [
'addon\huaweipay\event\PayClose'
],
'PayRefund' => [
'addon\huaweipay\event\PayRefund'
],
'PayTransfer' => [
'addon\huaweipay\event\PayTransfer'
],
'TransferType' => [
'addon\huaweipay\event\TransferType'
],
'AuthcodePay' => [
'addon\huaweipay\event\AuthcodePay'
],
'PayOrderQuery' => [
'addon\huaweipay\event\PayOrderQuery'
],
],
'subscribe' => [
],
];

View File

@@ -0,0 +1,15 @@
<?php
/**
* 华为支付扩展基本配置
*/
return [
'name' => 'huaweipay',
'title' => '华为支付',
'description' => '华为支付功能',
'type' => 'system', //插件类型 system :系统插件(自动安装), business:业务插件 promotion:扩展营销插件 tool:工具插件
'status' => 1,
'author' => '',
'version' => '5.3.1',
'version_no' => '525231212001',
'content' => '',
];

View File

@@ -0,0 +1,18 @@
<?php
/**
* 华为支付店铺菜单配置
*/
return [
[
'name' => 'HUAWEI_PAY_CONFIG',
'title' => '华为支付编辑',
'url' => 'huaweipay://shop/pay/config',
'parent' => 'CONFIG_PAY',
'is_show' => 0,
'is_control' => 1,
'is_icon' => 0,
'picture' => '',
'picture_select' => '',
'sort' => 1,
],
];

View File

@@ -0,0 +1,291 @@
<?php
/**
* 华为支付客户端类
* 封装华为支付API调用
*/
namespace addon\huaweipay\data\sdk;
use think\facade\Log;
class HuaweiPayClient
{
// 华为支付网关地址
private $gatewayUrl = 'https://pay.cloud.huawei.com/gateway/api/pay';
// 华为支付配置
private $config = [];
// 签名算法
private $signType = 'RSA2';
/**
* 构造函数
* @param array $config 华为支付配置
*/
public function __construct($config)
{
$this->config = $config;
// 根据配置设置网关地址
if (isset($config['sandbox']) && $config['sandbox']) {
$this->gatewayUrl = 'https://pay-drcn.cloud.huawei.com/gateway/api/pay';
}
}
/**
* 生成签名
* @param array $params 请求参数
* @return string 签名结果
*/
private function generateSign($params)
{
// 移除空值和签名参数
$params = array_filter($params, function($value) {
return $value !== null && $value !== '';
});
unset($params['sign']);
unset($params['sign_type']);
// 按键名排序
ksort($params);
// 拼接参数
$stringToSign = '';
foreach ($params as $key => $value) {
$stringToSign .= $key . '=' . $value . '&';
}
$stringToSign = rtrim($stringToSign, '&');
// 根据签名类型生成签名
switch ($this->signType) {
case 'RSA2':
$privateKey = $this->config['private_key'];
$privateKey = "-----BEGIN PRIVATE KEY-----\n" . wordwrap($privateKey, 64, "\n", true) . "\n-----END PRIVATE KEY-----";
openssl_sign($stringToSign, $sign, $privateKey, OPENSSL_ALGO_SHA256);
break;
case 'RSA':
$privateKey = $this->config['private_key'];
$privateKey = "-----BEGIN PRIVATE KEY-----\n" . wordwrap($privateKey, 64, "\n", true) . "\n-----END PRIVATE KEY-----";
openssl_sign($stringToSign, $sign, $privateKey, OPENSSL_ALGO_SHA1);
break;
default:
throw new \Exception('不支持的签名类型');
}
return base64_encode($sign);
}
/**
* 验证签名
* @param array $params 响应参数
* @return bool 验证结果
*/
public function verifySign($params)
{
// 保存签名
$sign = $params['sign'] ?? '';
unset($params['sign']);
unset($params['sign_type']);
// 移除空值
$params = array_filter($params, function($value) {
return $value !== null && $value !== '';
});
// 按键名排序
ksort($params);
// 拼接参数
$stringToSign = '';
foreach ($params as $key => $value) {
$stringToSign .= $key . '=' . $value . '&';
}
$stringToSign = rtrim($stringToSign, '&');
// 验证签名
$huaweiPublicKey = $this->config['huawei_public_key'];
$huaweiPublicKey = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($huaweiPublicKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----";
switch ($this->signType) {
case 'RSA2':
$result = openssl_verify($stringToSign, base64_decode($sign), $huaweiPublicKey, OPENSSL_ALGO_SHA256);
break;
case 'RSA':
$result = openssl_verify($stringToSign, base64_decode($sign), $huaweiPublicKey, OPENSSL_ALGO_SHA1);
break;
default:
throw new \Exception('不支持的签名类型');
}
return $result === 1;
}
/**
* 发送HTTP请求
* @param string $url 请求地址
* @param array $params 请求参数
* @param string $method 请求方法
* @return array 响应结果
*/
private function httpRequest($url, $params, $method = 'POST')
{
$ch = curl_init();
// 设置请求头
$headers = [
'Content-Type: application/x-www-form-urlencoded; charset=utf-8',
];
// 生成签名
$params['app_id'] = $this->config['app_id'];
$params['sign_type'] = $this->signType;
$params['sign'] = $this->generateSign($params);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
} else {
$url .= '?' . http_build_query($params);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
if (curl_errno($ch)) {
throw new \Exception('HTTP请求错误: ' . curl_error($ch));
}
curl_close($ch);
// 解析响应
parse_str($response, $result);
return $result;
}
/**
* H5支付
* @param array $params 支付参数
* @param string $returnUrl 同步回调地址
* @param string $notifyUrl 异步回调地址
* @return array 支付结果
*/
public function h5Pay($params, $returnUrl, $notifyUrl)
{
$requestParams = [
'method' => 'h5pay.createPayment',
'version' => '1.0',
'timestamp' => date('Y-m-d H:i:s'),
'out_trade_no' => $params['out_trade_no'],
'subject' => $params['subject'],
'total_amount' => $params['total_amount'],
'body' => $params['body'],
'return_url' => $returnUrl,
'notify_url' => $notifyUrl,
'scene' => 'h5',
];
return $this->httpRequest($this->gatewayUrl, $requestParams);
}
/**
* APP支付
* @param array $params 支付参数
* @param string $notifyUrl 异步回调地址
* @return array 支付结果
*/
public function appPay($params, $notifyUrl)
{
$requestParams = [
'method' => 'apppay.createPayment',
'version' => '1.0',
'timestamp' => date('Y-m-d H:i:s'),
'out_trade_no' => $params['out_trade_no'],
'subject' => $params['subject'],
'total_amount' => $params['total_amount'],
'body' => $params['body'],
'notify_url' => $notifyUrl,
];
return $this->httpRequest($this->gatewayUrl, $requestParams);
}
/**
* 查询订单
* @param array $params 查询参数
* @return array 查询结果
*/
public function queryOrder($params)
{
$requestParams = [
'method' => 'unifiedorder.query',
'version' => '1.0',
'timestamp' => date('Y-m-d H:i:s'),
];
// 支持通过商户订单号或华为交易号查询
if (isset($params['out_trade_no'])) {
$requestParams['out_trade_no'] = $params['out_trade_no'];
} elseif (isset($params['trade_no'])) {
$requestParams['trade_no'] = $params['trade_no'];
} else {
throw new \Exception('查询参数错误必须提供out_trade_no或trade_no');
}
return $this->httpRequest($this->gatewayUrl, $requestParams);
}
/**
* 关闭订单
* @param array $params 关闭参数
* @return array 关闭结果
*/
public function closeOrder($params)
{
$requestParams = [
'method' => 'unifiedorder.close',
'version' => '1.0',
'timestamp' => date('Y-m-d H:i:s'),
'out_trade_no' => $params['out_trade_no'],
];
return $this->httpRequest($this->gatewayUrl, $requestParams);
}
/**
* 退款
* @param array $params 退款参数
* @return array 退款结果
*/
public function refund($params)
{
$requestParams = [
'method' => 'refund.apply',
'version' => '1.0',
'timestamp' => date('Y-m-d H:i:s'),
'out_trade_no' => $params['out_trade_no'],
'trade_no' => $params['trade_no'],
'refund_amount' => $params['refund_amount'],
'out_request_no' => $params['out_request_no'],
'refund_reason' => $params['refund_reason'] ?? '',
];
return $this->httpRequest($this->gatewayUrl, $requestParams);
}
/**
* 验证回调签名
* @param array $params 回调参数
* @return bool 验证结果
*/
public function verifyNotify($params)
{
return $this->verifySign($params);
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* 华为支付授权码支付处理
*/
namespace addon\huaweipay\event;
use addon\huaweipay\model\Pay as PayModel;
/**
* 授权码支付
*/
class AuthcodePay
{
/**
* 授权码支付处理
*/
public function handle($param)
{
if ($param[ "pay_type" ] == "huaweipay") {
$pay_model = new PayModel($param[ 'site_id' ]);
$res = $pay_model->authcodePay($param);
return $res;
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* 华为支付处理
*/
namespace addon\huaweipay\event;
use addon\huaweipay\model\Pay as PayModel;
/**
* 生成支付
*/
class Pay
{
/**
* 支付方式及配置
*/
public function handle($param)
{
if ($param[ "pay_type" ] == "huaweipay") {
if (in_array($param[ "app_type" ], [ "h5", "app", "pc", "hwapp" ])) {
$pay_model = new PayModel($param[ 'site_id' ]);
$res = $pay_model->pay($param);
return $res;
}
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* 华为支付关闭订单处理
*/
namespace addon\huaweipay\event;
use addon\huaweipay\model\Pay as PayModel;
/**
* 关闭支付订单
*/
class PayClose
{
/**
* 关闭支付订单处理
*/
public function handle($param)
{
if ($param[ "pay_type" ] == "huaweipay") {
$pay_model = new PayModel($param[ 'site_id' ]);
$res = $pay_model->close($param);
return $res;
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* 华为支付异步回调处理
*/
namespace addon\huaweipay\event;
use addon\huaweipay\model\Pay as PayModel;
/**
* 支付异步回调
*/
class PayNotify
{
/**
* 支付异步回调处理
*/
public function handle($param)
{
if ($param[ "pay_type" ] == "huaweipay") {
$pay_model = new PayModel($param[ 'site_id' ]);
$res = $pay_model->notify($param);
return $res;
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* 华为支付订单查询处理
*/
namespace addon\huaweipay\event;
use addon\huaweipay\model\Pay as PayModel;
/**
* 支付订单查询
*/
class PayOrderQuery
{
/**
* 支付订单查询处理
*/
public function handle($param)
{
if ($param[ "pay_type" ] == "huaweipay") {
$pay_model = new PayModel($param[ 'site_id' ]);
$res = $pay_model->query($param);
return $res;
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* 华为支付退款处理
*/
namespace addon\huaweipay\event;
use addon\huaweipay\model\Pay as PayModel;
/**
* 支付退款
*/
class PayRefund
{
/**
* 支付退款处理
*/
public function handle($param)
{
if ($param[ "pay_type" ] == "huaweipay") {
$pay_model = new PayModel($param[ 'site_id' ]);
$res = $pay_model->refund($param);
return $res;
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* 华为支付转账处理
*/
namespace addon\huaweipay\event;
use addon\huaweipay\model\Pay as PayModel;
/**
* 支付转账
*/
class PayTransfer
{
/**
* 支付转账处理
*/
public function handle($param)
{
if ($param[ "pay_type" ] == "huaweipay") {
$pay_model = new PayModel($param[ 'site_id' ]);
$res = $pay_model->transfer($param);
return $res;
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* 华为支付方式定义
*/
namespace addon\huaweipay\event;
use addon\huaweipay\model\Config;
/**
* 支付方式 (后台调用)
*/
class PayType
{
/**
* 支付方式及配置
*/
public function handle($param)
{
$app_type = $param['app_type'] ?? '';
if (!empty($app_type)) {
if (!in_array($app_type, [ "h5", "app", "pc", "hwapp" ])) {
return '';
}
$config_model = new Config();
$config_result = $config_model->getPayConfig($param[ 'site_id' ]);
$config = $config_result[ "data" ][ "value" ] ?? [];
$pay_status = $config[ "pay_status" ] ?? 0;
if ($pay_status == 0) {
return '';
}
}
$info = array (
"pay_type" => "huaweipay",
"pay_type_name" => "华为支付",
"edit_url" => "huaweipay://shop/pay/config",
"shop_url" => "huaweipay://shop/pay/config",
"logo" => "addon/huaweipay/icon.png",
"desc" => "华为支付(www.huawei.com) 是华为公司提供的网上支付平台。"
);
return $info;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* 华为支付转账类型处理
*/
namespace addon\huaweipay\event;
/**
* 转账类型
*/
class TransferType
{
/**
* 转账类型处理
*/
public function handle($param)
{
$info = array (
"pay_type" => "huaweipay",
"pay_type_name" => "华为支付",
"logo" => "addon/huaweipay/icon.png",
);
return $info;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,79 @@
<?php
/**
* 华为支付配置模型
*/
namespace addon\huaweipay\model;
use app\model\system\Config as ConfigModel;
use app\model\BaseModel;
/**
* 华为支付配置
*/
class Config extends BaseModel
{
private $encrypt = '******';
/**
* 设置支付配置
* @param $data
* @param int $site_id
* @param string $app_module
* @return array
*/
public function setPayConfig($data, $site_id = 0, $app_module = 'shop')
{
$config = new ConfigModel();
// 未加密前的数据
$original_config = $this->getPayConfig($site_id)[ 'data' ][ 'value' ] ?? [];
// 检测数据是否发生变化,如果没有变化,则保持未加密前的数据
if (!empty($data[ 'app_id' ]) && $data[ 'app_id' ] == $this->encrypt) {
$data[ 'app_id' ] = $original_config[ 'app_id' ]; // 应用ID
}
if (!empty($data[ 'private_key' ]) && $data[ 'private_key' ] == $this->encrypt) {
$data[ 'private_key' ] = $original_config[ 'private_key' ]; // 应用私钥
}
if (!empty($data[ 'public_key' ]) && $data[ 'public_key' ] == $this->encrypt) {
$data[ 'public_key' ] = $original_config[ 'public_key' ]; // 应用公钥
}
if (!empty($data[ 'huawei_public_key' ]) && $data[ 'huawei_public_key' ] == $this->encrypt) {
$data[ 'huawei_public_key' ] = $original_config[ 'huawei_public_key' ]; // 华为公钥
}
$res = $config->setConfig($data, '华为支付配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'HUAWEI_PAY_CONFIG' ] ]);
return $res;
}
/**
* 获取支付配置
* @param int $site_id
* @param string $app_module
* @param bool $need_encrypt 是否需要加密数据true加密、false不加密
* @return array
*/
public function getPayConfig($site_id = 0, $app_module = 'shop', $need_encrypt = false)
{
$config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'HUAWEI_PAY_CONFIG' ] ]);
if ($need_encrypt) {
// 加密敏感信息
if (!empty($res[ 'data' ][ 'value' ][ 'app_id' ])) {
$res[ 'data' ][ 'value' ][ 'app_id' ] = $this->encrypt; // 应用ID
}
if (!empty($res[ 'data' ][ 'value' ][ 'private_key' ])) {
$res[ 'data' ][ 'value' ][ 'private_key' ] = $this->encrypt; // 应用私钥
}
if (!empty($res[ 'data' ][ 'value' ][ 'public_key' ])) {
$res[ 'data' ][ 'value' ][ 'public_key' ] = $this->encrypt; // 应用公钥
}
if (!empty($res[ 'data' ][ 'value' ][ 'huawei_public_key' ])) {
$res[ 'data' ][ 'value' ][ 'huawei_public_key' ] = $this->encrypt; // 华为公钥
}
}
return $res;
}
}

View File

@@ -0,0 +1,252 @@
<?php
/**
* 华为支付模型
*/
namespace addon\huaweipay\model;
use addon\huaweipay\data\sdk\HuaweiPayClient;
use app\model\BaseModel;
use app\model\system\Cron;
use app\model\system\Pay as PayCommon;
use app\model\system\Pay as PayModel;
use think\facade\Log;
/**
* 华为支付配置
*/
class Pay extends BaseModel
{
public $hwpay_client;
/**
* 构造函数
* @param $site_id 站点ID
*/
function __construct($site_id)
{
try {
// 获取华为支付参数
$config_info = (new Config())->getPayConfig($site_id)['data']['value'];
if (!empty($config_info)) {
// 初始化华为支付客户端
$this->hwpay_client = new HuaweiPayClient($config_info);
}
} catch (\Exception $e) {
return $this->error('', '华为支付配置错误: ' . $e->getMessage());
}
}
/**
* 生成支付
* @param $param 支付参数
* @return array
*/
public function pay($param)
{
try {
// 构造华为支付请求参数
$parameter = array(
"out_trade_no" => $param["out_trade_no"],
"subject" => substr($param["pay_body"], 0, 15),
"total_amount" => (float)$param["pay_money"],
"body" => substr($param["pay_body"], 0, 60),
);
// 绑定商户数据
$pay_model = new PayModel();
$pay_model->bindMchPay($param["out_trade_no"], []);
// 根据不同的应用类型调用不同的华为支付API
switch ($param["app_type"]) {
case "h5":
// H5支付
$result = $this->hwpay_client->h5Pay($parameter, $param["return_url"], $param["notify_url"]);
if ($result['code'] == '0') {
return $this->success([
'type' => 'url',
'data' => $result['pay_url']
]);
} else {
return $this->error('', $result['msg'] ?? '华为支付请求失败');
}
break;
case "app":
// APP支付
$result = $this->hwpay_client->appPay($parameter, $param["notify_url"]);
if ($result['code'] == '0') {
return $this->success([
'type' => 'params',
'data' => $result
]);
} else {
return $this->error('', $result['msg'] ?? '华为支付请求失败');
}
break;
default:
// 默认返回错误
return $this->error('', '不支持的支付类型');
}
} catch (\Exception $e) {
Log::error('华为支付生成支付失败: ' . $e->getMessage());
return $this->error('', $e->getMessage());
}
}
/**
* 支付关闭
* @param $param 关闭订单参数
* @return array
*/
public function close($param)
{
try {
$parameter = array(
"out_trade_no" => $param["out_trade_no"]
);
// 调用华为支付关闭订单API
$result = $this->hwpay_client->closeOrder($parameter);
if ($result['code'] == '0') {
return $this->success();
} else {
return $this->error('', $result['msg'] ?? '关闭订单失败');
}
} catch (\Exception $e) {
Log::error('华为支付关闭订单失败: ' . $e->getMessage());
return $this->error('', $e->getMessage());
}
}
/**
* 华为支付退款
* @param array $param 退款参数
* @return array
*/
public function refund($param)
{
try {
$pay_info = $param["pay_info"];
$refund_no = $param["refund_no"];
$out_trade_no = $pay_info["out_trade_no"] ?? '';
$trade_no = $pay_info["trade_no"] ?? '';
$refund_fee = $param["refund_fee"];
$parameter = array(
'out_trade_no' => $out_trade_no,
'trade_no' => $trade_no,
'refund_amount' => sprintf("%.2f", $refund_fee),
'out_request_no' => $refund_no,
'refund_reason' => $param["refund_desc"] ?? ''
);
// 调用华为支付退款API
$result = $this->hwpay_client->refund($parameter);
if ($result['code'] == '0') {
return $this->success();
} else {
return $this->error('', $result['msg'] ?? '退款失败');
}
} catch (\Exception $e) {
Log::error('华为支付退款失败: ' . $e->getMessage());
return $this->error('', $e->getMessage());
}
}
/**
* 华为支付转账
* @param $param 转账参数
* @return array
*/
public function transfer($param)
{
try {
// 华为支付转账功能需要根据实际API进行实现
// 目前华为支付客户端未实现转账功能,需要根据官方文档扩展
return $this->error('', '华为支付转账功能暂未实现');
} catch (\Exception $e) {
Log::error('华为支付转账失败: ' . $e->getMessage());
return $this->error('', $e->getMessage());
}
}
/**
* 异步完成支付
* @param $param 回调参数
*/
public function notify($param)
{
try {
// 验证华为支付回调签名
$is_valid = $this->hwpay_client->verifyNotify($param);
if ($is_valid) { // 验证成功
$out_trade_no = $param['out_trade_no'];
// 华为支付交易号
$trade_no = $param['trade_no'];
// 交易状态
$trade_status = $param['trade_status'];
$pay_common = new PayCommon();
if ($trade_status == "TRADE_SUCCESS") {
$retval = $pay_common->onlinePay($out_trade_no, "huaweipay", $trade_no, "huaweipay");
}
echo "success";
} else {
// 验证失败
Log::error('华为支付回调签名验证失败: ' . json_encode($param));
echo "fail";
}
} catch (\Exception $e) {
Log::error('华为支付回调处理失败: ' . $e->getMessage());
echo "fail";
}
}
/**
* 授权码支付
* @param $param 授权码支付参数
* @return array|mixed|void
*/
public function authcodePay($param)
{
try {
// 华为支付授权码支付功能需要根据实际API进行实现
// 目前华为支付客户端未实现授权码支付功能,需要根据官方文档扩展
return $this->error('', '华为支付授权码支付功能暂未实现');
} catch (\Exception $e) {
Log::error('华为支付授权码支付失败: ' . $e->getMessage());
return $this->error('', $e->getMessage());
}
}
/**
* 查询订单信息
* @param $param 查询参数
* @return array
*/
public function query($param)
{
try {
// 构造查询请求参数
$parameter = array(
"out_trade_no" => $param["out_trade_no"],
);
// 调用华为支付查询订单API
$result = $this->hwpay_client->queryOrder($parameter);
if ($result['code'] == '0') {
return $this->success($result['data']);
} else {
return $this->error('', $result['msg'] ?? '查询订单失败');
}
} catch (\Exception $e) {
Log::error('华为支付查询订单失败: ' . $e->getMessage());
return $this->error('', $e->getMessage());
}
}
}

View File

@@ -0,0 +1,82 @@
<?php
/**
* 华为支付控制器
*/
namespace addon\huaweipay\shop\controller;
use addon\huaweipay\model\Config as ConfigModel;
use app\shop\controller\BaseShop;
use think\facade\Config;
use app\model\upload\Upload;
/**
* 华为支付 控制器
*/
class Pay extends BaseShop
{
public function config()
{
$config_model = new ConfigModel();
if (request()->isJson()) {
$app_id = input("app_id", "");//华为应用ID, // PETALPAY.APPID, 商户号关联的APPID
$mch_id = input("mch_id", "");//商户号, // PETALPAY.MERC_NO, 商户号
$private_key = input("private_key", "");//商户应用私钥, // PETALPAY.MERC_PRIVATE_KEY, 商户应用私钥
$mch_auth_id = input("mch_auth_id", "");//商户证书id, // PETALPAY.MERC_AUTH_ID, 商户证书id
$sign_type = input("sign_type", "RSA");//商户公私钥签名类型, // PETALPAY.SIGN_TYPE, 签名类型, 默认使用RSA
$huawei_public_key = input("huawei_public_key", ""); //华为公钥, // PETALPAY.HW_PAY_PUBLIC_KEY_FOR_CALLBACK, 华为公钥
$app_type = input("app_type", "");//支持端口 如web app
$pay_status = input("pay_status", 0);//支付启用状态
$refund_status = input("refund_status", 0);//退款启用状态
$transfer_status = input("transfer_status", 0);//转账启用状态
$data = array (
"app_id" => $app_id,
"mch_id" => $mch_id,
"private_key" => $private_key,
"mch_auth_id" => $mch_auth_id,
"sign_type" => $sign_type,
"huawei_public_key" => $huawei_public_key,
"refund_status" => $refund_status,
"pay_status" => $pay_status,
"transfer_status" => $transfer_status,
"app_type" => $app_type
);
$result = $config_model->setPayConfig($data, $this->site_id, $this->app_module);
return $result;
} else {
$info = $config_model->getPayConfig($this->site_id, $this->app_module, true)[ 'data' ][ 'value' ] ?? [];
if (!empty($info)) {
$app_type_arr = [];
if (!empty($info[ 'app_type' ])) {
$app_type_arr = explode(',', $info[ 'app_type' ]);
}
$info[ 'app_type_arr' ] = $app_type_arr;
}
$this->assign("info", $info);
$this->assign("app_type", Config::get("app_type"));
return $this->fetch("pay/config");
}
}
/**
* 上传华为支付证书
*/
public function uploadHuaweiCrt()
{
$upload_model = new Upload();
$site_id = request()->siteid();
$name = input("name", "");
$extend_type = [ 'crt', 'pem' ];
$param = array (
"name" => "file",
"extend_type" => $extend_type
);
$site_id = max($site_id, 0);
$result = $upload_model->setPath("common/huaweipay/crt/" . $site_id . "/")->file($param);
return $result;
}
}

View File

@@ -0,0 +1,219 @@
<style>
.input-text span{margin-right: 15px;}
.file-upload {display: inline-block; margin-right: 5px;}
</style>
<div class="layui-form form-wrap">
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>商户号:</label>
<div class="layui-input-block">
<input name="mch_id" type="text" value="{$info.mch_id ?? ''}" class="layui-input len-long" lay-verify="required">
</div>
<div class="word-aux"><span>[MERC_NO]</span>华为支付商户号 <a href="https://developer.huawei.com/consumer/cn/doc/pay-docs/hwzf-shanghuhao-0000001725982508" target="_blank">查看指引</a></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>华为应用ID</label>
<div class="layui-input-block">
<input name="app_id" type="text" value="{$info.app_id ?? ''}" class="layui-input len-long" lay-verify="required">
</div>
<div class="word-aux">华为分配给开发者的应用ID <a href="https://developer.huawei.com/consumer/cn/doc/pay-docs/hwzf-appidguanli-0000001757041165" target="_blank">查看指引</a></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>商户应用私钥:</label>
<div class="layui-input-block">
{notempty name="$info.private_key"}
<p class="file-upload">已上传</p>
{else/}
<p class="file-upload">未上传</p>
{/notempty}
<button type="button" class="layui-btn" id="private_key_upload">
<i class="layui-icon">&#xe67c;</i>上传文件
</button>
<input type="hidden" name="private_key" class="layui-input len-long" value="{$info.private_key ?? ''}" lay-verify="private_key">
</div>
<div class="word-aux">上传商户应用私钥.pem 文件 </div>
<div class="word-aux">如何获取商户应用私钥.pem文件 <a href="https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/certificate-preparation-0000001596094962#section1886895941411" target="_blank">查看指引</a></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>商户证书ID</label>
<div class="layui-input-block">
<input name="mch_auth_id" type="text" value="{$info.mch_auth_id ?? ''}" class="layui-input len-long" lay-verify="required">
</div>
<div class="word-aux">商户证书ID <a href="https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/certificate-preparation-0000001596094962#section64183273115" target="_blank">如何获得商户证书ID查看指引</a></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>华为支付证书:</label>
<div class="layui-input-block">
{notempty name="$info.huawei_public_key"}
<p class="file-upload">已上传</p>
{else/}
<p class="file-upload">未上传</p>
{/notempty}
<button type="button" class="layui-btn" id="huawei_public_key_upload">
<i class="layui-icon">&#xe67c;</i>上传文件
</button>
<input type="hidden" name="huawei_public_key" class="layui-input len-long" value="{$info.huawei_public_key ?? ''}" lay-verify="huawei_public_key">
</div>
<div class="word-aux">上传华为支付证书.pem文件</div>
<div class="word-aux">如何获取华为支付证书.pem文件<a href="https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/certificate-preparation-0000001596094962#section377312802116" target="_blank">查看指引</a></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">(可选)华为支付服务加密公钥:</label>
<div class="layui-input-block">
{notempty name="$info.huawei_public_key_for_sessionkey"}
<p class="file-upload">已上传</p>
{else/}
<p class="file-upload">未上传</p>
{/notempty}
<button type="button" class="layui-btn" id="huawei_public_key_for_sessionkey_upload">
<i class="layui-icon">&#xe67c;</i>上传文件
</button>
<input type="hidden" name="huawei_public_key_for_sessionkey" class="layui-input len-long" value="{$info.huawei_public_key_for_sessionkey ?? ''}">
</div>
<div class="word-aux">上传华为支付服务加密公钥.pem文件</div>
<div class="word-aux">(可选)加密公钥, 没有可以不填 <a href="https://developer.huawei.com/consumer/cn/doc/HMSCore-Guides/certificate-preparation-0000001596094962#section377312802116" target="_blank">查看指引</a></div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>支持端口:</label>
<div class="input-text">
{foreach $app_type as $app_type_k => $app_type_v}
{if condition="$app_type_v['name'] !='微信小程序' && $app_type_v['name'] !='微信公众号'"}
<span>{$app_type_v['name']}</span>
{/if}
{/foreach}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>是否启用支付:</label>
<div class="layui-input-block">
<input type="checkbox" name="pay_status" value="1" lay-skin="switch" {if condition="$info && $info.pay_status == 1"} checked {/if} />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>是否启用退款:</label>
<div class="layui-input-block">
<input type="checkbox" name="refund_status" value="1" lay-skin="switch" {if condition="$info && $info.refund_status == 1"} checked {/if} />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>是否启用转账:</label>
<div class="layui-input-block">
<input type="checkbox" name="transfer_status" value="1" lay-skin="switch" {if condition="$info && $info.transfer_status == 1"} checked {/if} />
</div>
</div>
<div class="form-row">
<button class="layui-btn" lay-submit lay-filter="save">保存</button>
<button class="layui-btn layui-btn-primary" onclick="back()">返回</button>
</div>
</div>
<script>
layui.use('form', function() {
var form = layui.form;
var repeat_flag = false; //防重复标识
form.render();
// 上传商户应用私钥.pem 文件
new Upload({
el: '#private_key',
url: ns.url("huaweipay://shop/pay/uploadHuaweiCrt"),
accept: 'file',
callback: function(res){
if (res.code >= 0) {
$('input[name="private_key"]').val(res.data.path);
$('input[name="private_key"]').siblings(".file-upload").text("已上传");
}
}
});
// 上传华为支付服务加密公钥.pem文件
new Upload({
el: '#huawei_public_key_for_sessionkey_upload',
url: ns.url("huaweipay://shop/pay/uploadHuaweiCrt"),
accept: 'file',
callback: function(res){
if (res.code >= 0) {
$('input[name="huawei_public_key_for_sessionkey"]').val(res.data.path);
$('input[name="huawei_public_key_for_sessionkey"]').siblings(".file-upload").text("已上传");
}
}
});
// 上传华为支付证书.pem文件
new Upload({
el: '#huawei_public_key_upload',
url: ns.url("huaweipay://shop/pay/uploadHuaweiCrt"),
accept: 'file',
callback: function(res){
if (res.code >= 0) {
$('input[name="huawei_public_key"]').val(res.data.path);
$('input[name="huawei_public_key"]').siblings(".file-upload").text("已上传");
}
}
});
/**
* 表单验证
*/
form.verify({
private_key: function(value){
if (!/[\S]+/.test(value)) return '请上传商户应用私钥.pem文件';
},
huawei_public_key: function(value){
if (!/[\S]+/.test(value)) return '请上传华为支付证书.pem文件';
},
// 可选
// huawei_public_key_for_sessionkey: function(value){
// if (!/[\S]+/.test(value)) return '请上传华为支付服务加密公钥.pem文件';
// }
});
/**
* 监听提交
*/
form.on('submit(save)', function(data) {
if (repeat_flag) return false;
repeat_flag = true;
$.ajax({
url: ns.url("huaweipay://shop/pay/config"),
data: data.field,
dataType: 'JSON',
type: 'POST',
success: function(res){
repeat_flag = false;
if (res.code >= 0) {
layer.confirm('编辑成功', {
title:'操作提示',
btn: ['返回列表', '继续编辑'],
yes: function(index, layero) {
location.hash = ns.hash("shop/config/pay");
layer.close(index);
},
btn2: function(index, layero) {
layer.close(index);
}
});
}else{
layer.msg(res.message);
}
}
});
});
});
function back(){
location.hash = ns.hash("shop/config/pay");
}
</script>

View File

@@ -0,0 +1,71 @@
<?php
namespace addon\offlinepay\api\controller;
use addon\offlinepay\model\Config as ConfigModel;
use addon\offlinepay\model\Pay as PayModel;
use app\api\controller\BaseApi;
use app\model\upload\Upload as UploadModel;
/**
* 线下支付
*/
class Pay extends BaseApi
{
/**
* 配置
*/
public function config()
{
$config_model = new ConfigModel();
$res = $config_model->getPayConfig($this->site_id);
return $this->response($res);
}
/**
* 信息
*/
public function info()
{
$token = $this->checkToken();
if ($token[ 'code' ] < 0) return $this->response($token);
$out_trade_no = $this->params['out_trade_no'] ?? '';
$pay_model = new PayModel();
$res = $pay_model->getInfo([['out_trade_no', '=', $out_trade_no], ['member_id', '=', $this->member_id]]);
return $this->response($res);
}
/**
* 支付
*/
public function pay()
{
$token = $this->checkToken();
if ($token[ 'code' ] < 0) return $this->response($token);
$pay_model = new PayModel();
$res = $pay_model->pay([
'member_id' => $this->member_id,
'out_trade_no' => $this->params['out_trade_no'] ?? '',
'imgs' => $this->params['imgs'] ?? '',
'desc' => $this->params['desc'] ?? ''
]);
return $this->response($res);
}
/**
* 图片上传
*/
public function uploadImg()
{
$upload_model = new UploadModel(0);
$param = [
'thumb_type' => '',
'name' => 'file',
'cloud' => 1,
];
$result = $upload_model->setPath('offlinepay/' . date('Ymd') . '/')->image($param);
return $this->response($result);
}
}

View File

@@ -0,0 +1,30 @@
<?php
return [
// 自定义模板页面类型,格式:[ 'title' => '页面类型名称', 'name' => '页面标识', 'path' => '页面路径', 'value' => '页面数据json格式' ]
'template' => [],
// 后台自定义组件——装修
'util' => [],
// 自定义页面路径
'link' => [],
// 自定义图标库
'icon_library' => [],
// uni-app 组件,格式:[ 'name' => '组件名称/文件夹名称', 'path' => '文件路径/目录路径' ]多个逗号隔开自定义组件名称前缀必须是diy-,也可以引用第三方组件
'component' => [],
// uni-app 页面,多个逗号隔开
'pages' => [],
// 模板信息,格式:'title' => '模板名称', 'name' => '模板标识', 'cover' => '模板封面图', 'preview' => '模板预览图', 'desc' => '模板描述'
'info' => [],
// 主题风格配色格式可以自由定义扩展【在uni-app中通过this.themeStyle... 获取定义的颜色字段例如this.themeStyle.main_color】
'theme' => [],
// 自定义页面数据,格式:[ 'title' => '页面名称', 'name' => "页面标识", 'value' => [页面数据json格式] ]
'data' => []
];

View File

@@ -0,0 +1,30 @@
<?php
// 事件定义文件
return [
'bind' => [
],
'listen' => [
//支付方式,后台查询
'PayType' => [
'addon\offlinepay\event\PayType'
],
'Pay' => [
'addon\offlinepay\event\Pay'
],
'PayClose' => [
'addon\offlinepay\event\PayClose'
],
'PayRefund' => [
'addon\offlinepay\event\PayRefund'
],
'SendMessageTemplate' => [
'addon\offlinepay\event\MessageOfflinepayWaitAudit',
'addon\offlinepay\event\MessageOfflinepayAuditRefuse',
],
],
'subscribe' => [
],
];

View File

@@ -0,0 +1,12 @@
<?php
return [
'name' => 'offlinepay',
'title' => '线下支付',
'description' => '线下支付功能',
'type' => 'system', //插件类型 system :系统插件(自动安装), business:业务插件 promotion:扩展营销插件 tool:工具插件
'status' => 1,
'author' => '',
'version' => '5.5.2',
'version_no' => '552250604001',
'content' => '',
];

View File

@@ -0,0 +1,49 @@
<?php
// +----------------------------------------------------------------------
// | 平台端菜单设置
// +----------------------------------------------------------------------
return [
[
'name' => 'OFFLINE_PAY_CONFIG',
'title' => '线下支付编辑',
'url' => 'offlinepay://shop/pay/config',
'parent' => 'CONFIG_PAY',
'is_show' => 0,
'is_control' => 1,
'is_icon' => 0,
'sort' => 1,
'type' => 'button',
],
[
'name' => 'OFFLINE_PAY_LIST',
'title' => '线下支付',
'url' => 'offlinepay://shop/pay/lists',
'parent' => 'ORDER_MANAGE',
'is_show' => 1,
'is_control' => 1,
'is_icon' => 0,
'sort' => 8,
'child_list' => [
[
'name' => 'OFFLINE_PAY_AUDIT_PASS',
'title' => '审核通过',
'url' => 'offlinepay://shop/pay/auditpass',
'is_show' => 0,
'is_control' => 1,
'is_icon' => 0,
'sort' => 1,
'type' => 'button',
],
[
'name' => 'OFFLINE_PAY_AUDIT_REFUSE',
'title' => '审核拒绝',
'url' => 'offlinepay://shop/pay/auditrefuse',
'is_show' => 0,
'is_control' => 1,
'is_icon' => 0,
'sort' => 2,
'type' => 'button',
],
]
],
];

View File

@@ -0,0 +1,17 @@
<?php
namespace addon\offlinepay\event;
/**
* 应用安装
*/
class Install
{
/**
* 执行安装
*/
public function handle()
{
return success();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace addon\offlinepay\event;
use addon\offlinepay\model\Pay as PayModel;
class MessageOfflinepayAuditRefuse
{
public function handle($param)
{
if ($param["keywords"] == "OFFLINEPAY_AUDIT_REFUSE") {
$pay_model = new PayModel();
return $pay_model->messageAuditRefuse($param);
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace addon\offlinepay\event;
use addon\offlinepay\model\Pay as PayModel;
class MessageOfflinepayWaitAudit
{
public function handle($param)
{
if ($param["keywords"] == "OFFLINEPAY_WAIT_AUDIT") {
$pay_model = new PayModel();
return $pay_model->messageWaitAudit($param);
}
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace addon\offlinepay\event;
use app\model\system\Pay as PayModel;
/**
* 生成支付
*/
class Pay
{
/**
* 支付
*/
public function handle($params)
{
if ($params[ "pay_type" ] == "offlinepay") {
$pay_model = new PayModel();
$clear_res = $pay_model->clearMchPay($params[ "out_trade_no" ], 'offlinepay');
if($clear_res['code'] < 0) return $clear_res;
return success();
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace addon\offlinepay\event;
use addon\offlinepay\model\Pay as PayModel;
/**
* 关闭支付
*/
class PayClose
{
/**
* 关闭支付
* @param $params
* @return array
*/
public function handle($params)
{
$mch_info = json_decode($params['mch_info'], true);
$pay_type = $mch_info['pay_type'] ?? '';
if($pay_type == PayModel::PAY_TYPE){
$pay_model = new PayModel();
$result = $pay_model->close([['out_trade_no', '=', $params['out_trade_no']]]);
return $result;
}
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace addon\offlinepay\event;
use addon\offlinepay\model\Pay as PayModel;
/**
* 关闭支付
*/
class PayRefund
{
/**
* 关闭支付
* @param $params
* @return array
*/
public function handle($params)
{
if ($params[ "pay_info" ][ "pay_type" ] == PayModel::PAY_TYPE) {
$pay_model = new PayModel();
return $pay_model->refund($params['pay_info']['out_trade_no'], $params['refund_fee']);
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace addon\offlinepay\event;
use addon\offlinepay\model\Config;
/**
* 支付方式 (前后台调用)
*/
class PayType
{
/**
* 支付方式及配置
*/
public function handle($params)
{
$config_model = new Config();
$config_result = $config_model->getPayConfig($params[ 'site_id' ] ?? 1);
$config = $config_result[ "data" ][ "value" ] ?? [];
$pay_status = $config[ "pay_status" ] ?? 0;
$app_type = $params['app_type'] ?? '';
if (!empty($app_type)) {
$app_type_array = [ 'h5', 'wechat', 'weapp', 'pc' ];
if (!in_array($app_type, $app_type_array)) {
return '';
}
if ($pay_status == 0) {
return '';
}
}
$info = array (
"pay_type" => "offlinepay",
"pay_type_name" => "线下支付",
"edit_url" => "offlinepay://shop/pay/config",
"shop_url" => "offlinepay://shop/pay/config",
"logo" => "addon/offlinepay/icon.png",
"desc" => "通过银行卡、支付宝或微信收款码线下收款。",
"pay_status" => $pay_status
);
return $info;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace addon\offlinepay\event;
/**
* 应用卸载
*/
class UnInstall
{
/**
* 执行卸载
*/
public function handle()
{
return success();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 B

View File

@@ -0,0 +1,71 @@
<?php
namespace addon\offlinepay\model;
use app\model\system\Config as ConfigModel;
use app\model\BaseModel;
/**
* 线下支付配置
*/
class Config extends BaseModel
{
/**
* 设置支付配置
* @param $data
* @param $site_id
* @param $app_module
* @return array
*/
public function setPayConfig($data, $site_id = 0, $app_module = 'shop')
{
$data = $this->handleConfigData($data);
$config = new ConfigModel();
$res = $config->setConfig($data, '线下支付配置', 1, [ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'OFFLINE_PAY_CONFIG' ] ]);
return $res;
}
/**
* 获取支付配置
* @param $site_id
* @param $app_module
* @return array
*/
public function getPayConfig($site_id = 0, $app_module = 'shop')
{
$config = new ConfigModel();
$res = $config->getConfig([ [ 'site_id', '=', $site_id ], [ 'app_module', '=', $app_module ], [ 'config_key', '=', 'OFFLINE_PAY_CONFIG' ] ]);
$res['data']['value'] = $this->handleConfigData($res['data']['value']);
return $res;
}
/**
* 处理配置数据
* @param $data
* @return mixed
*/
protected function handleConfigData($data)
{
$default_config = [
'pay_status' => 0,//支付状态
'bank' => [
'status' => 0,//是否开启
'bank_name' => '',//银行名称
'account_name' => '',//账户名称
'account_number' => '',//账号
'branch_name' => '',//支行名称
],
'wechat' => [
'status' => 0,//是否开启
'account_name' => '',//账户名称
'payment_code' => '',//收款码
],
'alipay' => [
'status' => 0,//是否开启
'account_name' => '',//账户名称
'payment_code' => '',//收款码
],
];
return assignData($data, $default_config);
}
}

View File

@@ -0,0 +1,489 @@
<?php
namespace addon\offlinepay\model;
use addon\weapp\model\Message as WeappMessage;
use addon\wechat\model\Message as WechatMessage;
use app\dict\member_account\AccountDict;
use app\model\member\Member;
use app\model\member\MemberAccount;
use app\model\message\Message;
use app\model\message\Sms;
use app\model\shop\ShopAcceptMessage;
use app\model\system\Pay as PayModel;
use app\model\BaseModel;
use app\model\order\OrderCommon;
/**
* 微信支付配置
* 版本 1.0.4
*/
class Pay extends BaseModel
{
//支付类型
const PAY_TYPE = 'offlinepay';
//状态
const STATUS_WAIT_AUDIT = 0;
const STATUS_AUDIT_PASS = 1;
const STATUS_AUDIT_REFUSE = 2;
const STATUS_CLOSE = 3;
static public function getStatus($key = null)
{
$arr = [
[
'id' => self::STATUS_WAIT_AUDIT,
'name' => '待审核',
'const' => 'WAIT_AUDIT',
],
[
'id' => self::STATUS_AUDIT_PASS,
'name' => '审核通过',
'const' => 'AUDIT_PASS',
],
[
'id' => self::STATUS_AUDIT_REFUSE,
'name' => '审核拒绝',
'const' => 'AUDIT_REFUSE',
],
[
'id' => self::STATUS_CLOSE,
'name' => '已关闭',
'const' => 'CLOSE',
],
];
if(isset($arr[0][$key])){
$arr = array_column($arr, null, $key);
}
return $arr;
}
/**
* 支付操作
* @param $param
* @return array
*/
public function pay($param)
{
$member_id = $param['member_id'] ?? 0;
$out_trade_no = $param['out_trade_no'] ?? '';
$imgs = $param['imgs'] ?? '';
$desc = $param['desc'] ?? '';
if(empty($member_id)) return $this->error(null, '用户id不可为空');
if(empty($out_trade_no)) return $this->error(null, '外部交易号不可为空');
if(empty($imgs)) return $this->error(null, '请上传支付凭证');
$pay_model = new PayModel();
$pay_info = $pay_model->getPayInfo($out_trade_no)['data'];
if(empty($pay_info)) return $this->error(null, '支付信息有误');
if(!in_array($pay_info['pay_status'], [PayModel::PAY_STATUS_NOT, PayModel::PAY_STATUS_IN_PROCESS])){
return $this->error(null, '支付状态有误');
}
$offline_pay_info = model('pay_offline')->getInfo([['out_trade_no', '=', $out_trade_no], ['member_id', '=', $member_id]]);
if(!empty($offline_pay_info) && $offline_pay_info['status'] != self::STATUS_AUDIT_REFUSE){
return $this->error(null, '当前状态不可修改');
}
//记录线下支付信息
$data = [
'member_id' => $member_id,
'out_trade_no' => $out_trade_no,
'imgs' => $imgs,
'desc' => $desc,
'status' => self::STATUS_WAIT_AUDIT,
'update_time' => time(),
];
model('pay_offline')->startTrans();
try{
if(empty($offline_pay_info)){
$data['create_time'] = time();
model('pay_offline')->add($data);
//绑定支付数据
$pay_model->bindMchPay($out_trade_no, [
"pay_type" => 'offlinepay',
]);
}else{
model('pay_offline')->update($data, [['id', '=', $offline_pay_info['id']]]);
}
//支付信息修改
$update_data = ['pay_type' => self::PAY_TYPE, 'pay_status' => PayModel::PAY_STATUS_IN_PROCESS];
$pay_model->edit($update_data, [['out_trade_no', '=', $out_trade_no]]);
$pay_info = array_merge($pay_info, $update_data);
//具体业务处理
event('OfflinePay', $pay_info);
//发送消息
$message_model = new Message();
$message_model->sendMessage(['keywords' => 'OFFLINEPAY_WAIT_AUDIT', 'pay_info' => $pay_info, 'site_id' => $pay_info['site_id']]);
model('pay_offline')->commit();
return $this->success();
}catch(\Exception $e){
model('pay_offline')->rollback();
return $this->error(['file' => $e->getFile(), 'line' => $e->getLine(), 'message' => $e->getMessage()], $e->getMessage());
}
}
/**
* 审核通过
* @param $condition
* @return array|mixed|null
*/
public function auditPass($condition)
{
$offline_pay_info = $this->getInfo($condition)['data'];
if(empty($offline_pay_info)) return $this->error(null, '支付信息有误');
if($offline_pay_info['status'] != self::STATUS_WAIT_AUDIT) return $this->error(null, '不是待审核状态');
model('pay_offline')->startTrans();
try{
model('pay_offline')->update([
'status' => self::STATUS_AUDIT_PASS,
'update_time' => time(),
], $condition);
$pay_model = new PayModel();
$pay_res = $pay_model->onlinePay($offline_pay_info['out_trade_no'], self::PAY_TYPE, $offline_pay_info['out_trade_no'], 'offlinepay');
if($pay_res['code'] < 0){
model('pay_offline')->rollback();
return $pay_res;
}
model('pay_offline')->commit();
return $this->success();
}catch(\Exception $e){
model('pay_offline')->rollback();
return $this->error(['file' => $e->getFile(), 'line' => $e->getLine(), 'message' => $e->getMessage()], $e->getMessage());
}
}
/**
* 审核拒绝
* @param $condition
* @param $audit_remark
* @return array
*/
public function auditRefuse($condition, $audit_remark)
{
$offline_pay_info = $this->getInfo($condition)['data'];
if(empty($offline_pay_info)) return $this->error(null, '支付信息有误');
if($offline_pay_info['status'] != self::STATUS_WAIT_AUDIT) return $this->error(null, '不是待审核状态');
$pay_model = new PayModel();
$pay_info = $pay_model->getPayInfo($offline_pay_info['out_trade_no'])['data'];
if(empty($pay_info)) return $this->error(null, '支付信息有误');
model('pay_offline')->update([
'status' => self::STATUS_AUDIT_REFUSE,
'audit_remark' => $audit_remark,
'update_time' => time(),
], $condition);
//发送消息
$message_model = new Message();
$message_model->sendMessage(['keywords' => 'OFFLINEPAY_AUDIT_REFUSE', 'pay_info' => $pay_info, 'site_id' => $pay_info['site_id']]);
return $this->success();
}
/**
* 订单关闭取消处理
* @param $condition
* @return array
*/
public function close($condition)
{
$offline_pay_info = $this->getInfo($condition)['data'];
if(empty($offline_pay_info)) return $this->success();
if($offline_pay_info['status'] == self::STATUS_AUDIT_PASS) return $this->error(null, '线下支付审核通过不可以关闭');
if($offline_pay_info['status'] == self::STATUS_WAIT_AUDIT) return $this->error(null, '线下支付单据审核中不可以关闭');
model('pay_offline')->update([
'status' => self::STATUS_CLOSE,
'update_time' => time(),
], $condition);
return $this->success();
}
/**
* 退款
* @param $out_trade_no
* @param $refund_money
* @return array
*/
public function refund($out_trade_no, $refund_money)
{
$offline_pay_info = $this->getInfo([['out_trade_no', '=', $out_trade_no]])['data'];
if(empty($offline_pay_info)) return $this->error(null, '线下支付信息有误');
$pay_model = new PayModel();
$pay_info = $pay_model->getPayInfo($out_trade_no)['data'];
if(empty($pay_info)) return $this->error(null, '支付信息有误');
$member_account_model = new MemberAccount();
return $member_account_model->addMemberAccount($pay_info['site_id'], $offline_pay_info['member_id'], AccountDict::balance_money, $refund_money, 'refund', $pay_info['relate_id'], '订单退款返还!');
}
/**
* 获取信息
* @param $condition
* @param $field
* @return array
*/
public function getInfo($condition, $field = '*')
{
$info = model('pay_offline')->getInfo($condition, $field);
$info = $this->handleInfo($info);
return $this->success($info);
}
/**
* 获取分页列表
* @param $condition
* @param $page
* @param $page_size
* @param $order
* @param $field
* @param $alias
* @param $join
* @return array
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function getPageList($condition = [], $page = 1, $page_size = PAGE_LIST_ROWS, $order = '', $field = '*', $alias = 'a', $join = [])
{
$res = model('pay_offline')->pageList($condition, $field, $order, $page, $page_size, $alias, $join);
foreach($res['list'] as $key=>$val){
$res['list'][$key] = $this->handleInfo($val);
}
return $this->success($res);
}
/**
* 获取列表
* @param $condition
* @param $order
* @param $field
* @param $alias
* @param $join
* @param $group
* @return array
*/
public function getList($condition, $order = '', $field = '*', $alias = 'a', $join = [], $group = '')
{
$list = model('pay_offline')->getList($condition, $field, $order, $alias, $join, $group);
foreach($list as $key=>$val){
$list[$key] = $this->handleInfo($val);
}
return $this->success($list);
}
public function handleInfo($info)
{
if(isset($info['status'])){
$status_list = self::getStatus('id');
$info['status_info'] = $status_list[$info['status']] ?? null;
}
return $info;
}
/**
* 处理用户订单信息
* @param $order_info
* @return array
*/
public function handleMemberOrderInfo($order_info)
{
//字段检测
$fields = ['order_status','pay_type','out_trade_no','action'];
foreach($fields as $field){
if(!isset($order_info[$field])){
return $order_info;
}
}
if($order_info['order_status'] == OrderCommon::ORDER_CREATE && $order_info['pay_type'] == self::PAY_TYPE){
$offline_pay_info = $this->getInfo([['out_trade_no', '=', $order_info['out_trade_no']]])['data'];
if(!empty($offline_pay_info)){
$order_info['offline_pay_info'] = $offline_pay_info;
if(in_array($offline_pay_info['status'], [self::STATUS_WAIT_AUDIT, self::STATUS_AUDIT_REFUSE])){
foreach($order_info['action'] as $key=>$val){
if($val['action'] == 'orderPay'){
unset($order_info['action'][$key]);
}
}
}
if($offline_pay_info['status'] == self::STATUS_WAIT_AUDIT){
$order_info['order_status_name'] = '待审核';
}else if($offline_pay_info['status'] == self::STATUS_AUDIT_REFUSE){
$order_info['order_status_name'] = '审核拒绝';
$order_info['action'][] = [
'action' => 'orderOfflinePay',
'title' => '线下支付',
'color' => '',
];
}
$order_info['action'] = array_values($order_info['action']);
}
}
return $order_info;
}
/**
* 处理用户订单信息
* @param $order_info
* @return array
*/
public function handleAdminOrderInfo($order_info)
{
//字段检测
$fields = ['order_status','order_status_action','pay_type','out_trade_no'];
foreach($fields as $field){
if(!isset($order_info[$field])){
return $order_info;
}
}
if($order_info['order_status'] == OrderCommon::ORDER_CREATE){
$order_status_info = json_decode($order_info['order_status_action'], true);
if($order_info['pay_type'] == self::PAY_TYPE){
$offline_pay_info = $this->getInfo([['out_trade_no', '=', $order_info['out_trade_no']]])['data'];
$order_info['offline_pay_info'] = $offline_pay_info;
if(!empty($offline_pay_info)){
if($offline_pay_info['status'] == self::STATUS_WAIT_AUDIT){
$order_info['order_status_name'] = '待审核';
$order_status_info['action'][] = [
'action' => 'offlinePayAudit',
'title' => '支付审核',
'color' => '',
];
}else if($offline_pay_info['status'] == self::STATUS_AUDIT_REFUSE){
$order_info['status_name'] = '审核拒绝';
}
}
}else{
$order_status_info['action'][] = [
'action' => 'offlinePay',
'title' => '线下支付',
'color' => '',
];
}
$order_info['order_status_action'] = json_encode($order_status_info);
}
return $order_info;
}
/**
* 发送消息
* @param $param
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function messageWaitAudit($param)
{
$pay_info = $param['pay_info'];
$sms_model = new Sms();
$wechat_model = new WechatMessage();
$shop_accept_message_model = new ShopAcceptMessage();
$list = $shop_accept_message_model->getShopAcceptMessageList([['site_id', '=', $pay_info['site_id']]])['data'];
if (!empty($list)) {
foreach ($list as $v) {
if(!empty($v['mobile'])){
$message_data = [
'var_parse' => [
'order_name' => str_sub(replaceSpecialChar($pay_info[ 'pay_body' ]), 25),
'pay_money' => $pay_info['pay_money'],
'out_trade_no' => $pay_info['out_trade_no'],
],
'sms_account' => $v[ 'mobile' ],
];
$sms_model->sendMessage(array_merge($param, $message_data));
}
if (!empty($v[ 'wx_openid' ])) {
$message_data = [
'openid' => $v[ 'wx_openid' ],
'template_data' => [
'thing10' => str_sub($pay_info['pay_body'], 20),
'amount4' => $pay_info['pay_money'],
'character_string1' => $pay_info['out_trade_no'],
],
'page' => '',
];
$wechat_model->sendMessage(array_merge($param, $message_data));
}
}
}
}
/**
* 发送消息
* @param $param
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function messageAuditRefuse($param)
{
$pay_info = $param['pay_info'];
$sms_model = new Sms();
$wechat_model = new WechatMessage();
$weapp_model = new WeappMessage();
$member_model = new Member();
$member_info = $member_model->getMemberInfo([ [ 'member_id', '=', $pay_info[ 'member_id' ] ] ])[ 'data' ];
if(!empty($member_info['mobile'])){
$message_data = [
'var_parse' => [
'order_name' => str_sub(replaceSpecialChar($pay_info[ 'pay_body' ]), 25),
'pay_money' => $pay_info['pay_money'],
'out_trade_no' => $pay_info['out_trade_no'],
],
'sms_account' => $member_info[ 'mobile' ],
];
$sms_model->sendMessage(array_merge($param, $message_data));
}
if (!empty($member_info[ 'wx_openid' ])) {
$message_data = [
'openid' => $member_info[ 'wx_openid' ],
'template_data' => [
'thing10' => str_sub($pay_info['pay_body'], 20),
'amount6' => $pay_info['pay_money'],
'character_string5' => $pay_info['out_trade_no'],
],
'page' => 'pages/order/detail?order_id='.$pay_info['relate_id'],
];
$wechat_model->sendMessage(array_merge($param, $message_data));
}
if (!empty($member_info[ 'weapp_openid' ])) {
$message_data = [
'openid' => $member_info[ 'weapp_openid' ],
'template_data' => [
'thing2' => [
'value' => str_sub($pay_info['pay_body'], 20)
],
'amount3' => [
'value' => $pay_info['pay_money']
],
'character_string1' => [
'value' => $pay_info['out_trade_no']
],
'thing5' => [
'value' => '线下支付审核拒绝'
],
],
'page' => 'pages/order/detail?order_id='.$pay_info['relate_id'],
];
$weapp_model->sendMessage(array_merge($param, $message_data));
}
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace addon\offlinepay\shop\controller;
use addon\offlinepay\model\Config as ConfigModel;
use addon\offlinepay\model\Pay as PayModel;
use app\shop\controller\BaseShop;
/**
* 支付 控制器
*/
class Pay extends BaseShop
{
public function config()
{
$config_model = new ConfigModel();
if (request()->isJson()) {
$data = [
'pay_status' => input('pay_status', 0),//支付状态
'bank' => [
'status' => input('bank_status', 0),//是否开启
'bank_name' => input('bank_bank_name', ''),//银行名称
'account_name' => input('bank_account_name', ''),//账户名称
'account_number' => input('bank_account_number', ''),//账号
'branch_name' => input('bank_branch_name', ''),//支行名称
],
'wechat' => [
'status' => input('wechat_status', 0),//是否开启
'account_name' => input('wechat_account_name', ''),//账户名称
'payment_code' => input('wechat_payment_code', ''),//收款码
],
'alipay' => [
'status' => input('alipay_status', 0),//是否开启
'account_name' => input('alipay_account_name', ''),//账户名称
'payment_code' => input('alipay_payment_code', ''),//收款码
],
];
$result = $config_model->setPayConfig($data, $this->site_id, $this->app_module);
return $result;
} else {
$config_info = $config_model->getPayConfig($this->site_id, $this->app_module)[ 'data' ][ 'value' ];
$this->assign("config_info", $config_info);
return $this->fetch("pay/config");
}
}
public function lists()
{
if (request()->isJson()) {
$page_index = input('page', 1);
$page_size = input('page_size', PAGE_LIST_ROWS);
$status = input('status', 0);
$search_field = input('search_field', '');
$search_field_value = input('search_field_value', '');
$out_trade_no = input('out_trade_no', '');
$alias = 'po';
$join = [
['member m', 'm.member_id = po.member_id', 'left'],
['pay p', 'p.out_trade_no = po.out_trade_no', 'left'],
];
$field = [
'po.*',
'm.nickname,m.mobile',
'p.pay_detail,p.pay_money,p.event,p.relate_id',
];
$condition = [];
if($status !== 'all'){
$condition[] = ['po.status', '=', $status];
}
if($search_field_value != ''){
$condition[] = [$search_field, 'like', '%'.$search_field_value.'%'];
}
if($out_trade_no){
$condition[] = ['po.out_trade_no', '=', $out_trade_no];
}
$order = 'po.create_time desc';
$pay_model = new PayModel();
$res = $pay_model->getPageList($condition, $page_index, $page_size, $order, $field, $alias, $join);
//各种状态统计
foreach ($condition as $key=>$val){ if($val[0] == 'po.status') unset($condition[$key]); }
$condition = array_values($condition);
$status_num_list = $pay_model->getList($condition, '', 'count(*) as num, po.status', $alias, $join, 'po.status')['data'];
$status_num_data = array_column($status_num_list, 'num', 'status');
$res['data']['status_num_data'] = $status_num_data;
$res['c'] = $condition;
return $res;
} else {
$status_list = PayModel::getStatus();
$this->assign('status_list', $status_list);
$out_trade_no = input('out_trade_no', '');
$this->assign('out_trade_no', $out_trade_no);
return $this->fetch('pay/lists');
}
}
public function auditPass()
{
if(request()->isJson()){
$id = input('id', 0);
$pay_model = new PayModel();
return $pay_model->auditPass([['id', '=', $id]]);
}
}
public function auditRefuse()
{
if(request()->isJson()){
$id = input('id', 0);
$audit_remark = input('audit_remark', '');
$pay_model = new PayModel();
return $pay_model->auditRefuse([['id', '=', $id]], $audit_remark);
}
}
public function pay()
{
if(request()->isJson()){
$imgs = input('imgs', '');
$desc = input('desc', '');
$out_trade_no = input('out_trade_no', '');
$member_id = input('member_id', 0);
$pay_model = new PayModel();
//支付
$pay_res = $pay_model->pay([
'member_id' => $member_id,
'out_trade_no' => $out_trade_no,
'imgs' => $imgs,
'desc' => $desc,
]);
if($pay_res['code'] < 0) return $pay_res;
//审核
$audit_res = $pay_model->auditPass([
['out_trade_no', '=', $out_trade_no],
['member_id', '=', $member_id],
]);
return $audit_res;
}else{
$out_trade_no = input('out_trade_no', '');
$this->assign("out_trade_no", $out_trade_no);
$member_id = input('member_id', 0);
$this->assign("member_id", $member_id);
return $this->fetch("pay/pay");
}
}
public function test()
{
$out_trade_no = '171997599310581711000';
$member_id = 171;
$imgs = join(',', [
'http://b2cv4.com/upload/1/common/images/20240618/20240618105545171867934599817_BIG.jpg',
'http://b2cv4.com/upload/1/common/goods_grab/images/20240527/20240527032610171679477043213_BIG.jpg',
'http://b2cv4.com/upload/1/common/images/20240618/20240618105545171867934599817_BIG.jpg',
]);
$desc = '支付了33333次';
$pay_model = new PayModel();
$res = $pay_model->pay([
'member_id' => $member_id,
'out_trade_no' => $out_trade_no,
'imgs' => $imgs,
'desc' => $desc,
]);
dd($res);
}
}

View File

@@ -0,0 +1,193 @@
<div class="layui-form form-wrap">
<div class="layui-form-item">
<label class="layui-form-label">是否启用支付:</label>
<div class="layui-input-inline">
<input type="checkbox" name="pay_status" value="1" lay-skin="switch" {if condition="$config_info.pay_status == 1"} checked {/if} />
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">银行收款开启:</label>
<div class="layui-input-inline">
<input type="checkbox" name="bank_status" lay-filter="payment_type_status" value="1" lay-skin="switch" {if condition="$config_info.bank.status == 1"} checked {/if} />
</div>
</div>
<div class="payment-type bank {if $config_info.bank.status == 0}layui-hide{/if}">
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>银行名称:</label>
<div class="layui-input-inline">
<input name="bank_bank_name" type="text" value="{$config_info.bank.bank_name}" class="layui-input len-long" lay-verify="payment_type_data">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>账户名称:</label>
<div class="layui-input-inline">
<input name="bank_account_name" type="text" value="{$config_info.bank.account_name}" class="layui-input len-long" lay-verify="payment_type_data">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>账户账号:</label>
<div class="layui-input-inline">
<input name="bank_account_number" type="text" value="{$config_info.bank.account_number}" class="layui-input len-long" lay-verify="payment_type_data">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>支行名称:</label>
<div class="layui-input-inline">
<input name="bank_branch_name" type="text" value="{$config_info.bank.branch_name}" class="layui-input len-long" lay-verify="payment_type_data">
</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">微信收款开启:</label>
<div class="layui-input-inline">
<input type="checkbox" name="wechat_status" lay-filter="payment_type_status" value="1" lay-skin="switch" {if condition="$config_info.wechat.status == 1"} checked {/if} />
</div>
</div>
<div class="payment-type wechat {if $config_info.wechat.status == 0}layui-hide{/if}">
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>账户名称:</label>
<div class="layui-input-block">
<input name="wechat_account_name" type="text" value="{$config_info.wechat.account_name}" class="layui-input len-long" lay-verify="payment_type_data">
</div>
<div class="word-aux">请输入真实姓名,用于支付时确认收款人</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>收款码:</label>
<div class="layui-input-block" id="wechat_payment_code">
</div>
<div class="word-aux">请上传正方形的二维码,截图裁剪时尽量贴着二维码,不要有多余的空白</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">支付宝收款开启:</label>
<div class="layui-input-inline">
<input type="checkbox" name="alipay_status" lay-filter="payment_type_status" value="1" lay-skin="switch" {if condition="$config_info.alipay.status == 1"} checked {/if} />
</div>
</div>
<div class="payment-type alipay {if $config_info.alipay.status == 0}layui-hide{/if}">
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>账户名称:</label>
<div class="layui-input-block">
<input name="alipay_account_name" type="text" value="{$config_info.alipay.account_name}" class="layui-input len-long" lay-verify="payment_type_data">
</div>
<div class="word-aux">请输入真实姓名,用于支付时确认收款人</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"><span class="required">*</span>收款码:</label>
<div class="layui-input-block" id="alipay_payment_code">
</div>
<div class="word-aux">请上传正方形的二维码,截图裁剪时尽量贴着二维码,不要有多余的空白</div>
</div>
</div>
<div class="form-row">
<button class="layui-btn" lay-submit lay-filter="save">保存</button>
<button class="layui-btn layui-btn-primary" onclick="backPayConfig()">返回</button>
</div>
</div>
<script>
layui.use(['form'], function() {
var form = layui.form,
repeat_flag = false; //防重复标识
form.render();
new Upload({
container: '#wechat_payment_code',
name:'wechat_payment_code',
value:'{$config_info.wechat.payment_code}',
});
new Upload({
container: '#alipay_payment_code',
name:'alipay_payment_code',
value:'{$config_info.alipay.payment_code}',
});
form.on('switch(payment_type_status)', function (data) {
let name = $(data.elem).attr('name');
let payment_type = name.replace('_status', '');
let block = $(".payment-type."+payment_type);
if(data.elem.checked){
block.removeClass('layui-hide');
}else{
block.addClass('layui-hide');
}
})
form.verify({
payment_type_data: function(value, elem){
if(getPaymentTypeStatus(elem) && !value){
return '请输入'+getFieldName(elem);
}
},
wechat_payment_code:function (value, elem){
if(getPaymentTypeStatus(elem) && !value){
return '请上传'+getFieldName(elem);
}
},
alipay_payment_code:function (value, elem){
if(getPaymentTypeStatus(elem) && !value){
return '请上传'+getFieldName(elem);
}
},
})
function getPaymentTypeStatus(elem){
return $(elem).parents('.payment-type').prev().find('input[type=checkbox]').prop('checked');
}
function getFieldName(elem){
return $(elem).parents('.layui-form-item').find('.layui-form-label').text().replace('*', '').replace('', '');
}
/**
* 监听提交
*/
form.on('submit(save)', function(data) {
let pay_status = $('input[name="pay_status"]').prop('checked');
let bank_status = $('input[name="bank_status"]').prop('checked');
let wechat_status = $('input[name="wechat_status"]').prop('checked');
let alipay_status = $('input[name="alipay_status"]').prop('checked');
if(pay_status == 1 && (bank_status != 1 && wechat_status != 1 && alipay_status != 1)){
layer.msg('请至少开启一种收款方式');
return false;
}
if (repeat_flag) return false;
repeat_flag = true;
$.ajax({
url: ns.url("offlinepay://shop/pay/config"),
data: data.field,
dataType: 'JSON',
type: 'POST',
success: function(res) {
repeat_flag = false;
if (res.code == 0) {
layer.confirm('编辑成功', {
title:'操作提示',
btn: ['返回列表', '继续操作'],
yes: function(index, layero){
location.hash = ns.hash("shop/config/pay");
layer.close(index);
},
btn2: function(index, layero) {
layer.close(index);
}
});
}else{
layer.msg(res.message);
}
}
});
});
});
function backPayConfig() {
location.hash = ns.hash("shop/config/pay");
}
</script>

View File

@@ -0,0 +1,356 @@
<style>
.img-list{
display: flex;
}
.img-list .img-item{
width: 100px;
height: 100px;
margin-right: 10px;
border: 1px dashed #ccc;
position: relative;
}
.img-list .img-item img{
position: absolute;
left:50%;
top:50%;
transform: translate(-50%, -50%);
max-width: 90%;
max-height: 90%;
}
</style>
<!-- 搜索框 -->
<div class="screen layui-collapse" lay-filter="selection_panel">
<div class="layui-colla-item">
<form class="layui-colla-content layui-form layui-show">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">用户搜索</label>
<div class="layui-input-inline">
<select name="search_field" lay-filter="search_field">
<option value="m.nickname">昵称</option>
<option value="m.mobile">手机号</option>
<option value="po.out_trade_no">外部交易号</option>
<option value="p.pay_detail">支付信息</option>
</select>
</div>
<div class="layui-form-mid">&nbsp;</div>
<div class="layui-input-inline">
<input type="text" name="search_field_value" autocomplete="off" class="layui-input" placeholder="请输入"/>
</div>
</div>
</div>
<input type="hidden" name="status" value="{$status_list[0]['id']}"/>
<input type="hidden" name="out_trade_no" value="">
<div class="form-row">
<button class="layui-btn" lay-submit lay-filter="search">筛选</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</form>
</div>
</div>
<div class="layui-tab table-tab" lay-filter="table_list_tab" id="table_list_tab">
<ul class="layui-tab-title">
{foreach $status_list as $key=>$status_info}
<li class="{$key == 0 ? 'layui-this' : ''}" lay-id="{$status_info.id}">{$status_info.name}<span class="status-num layui-hide">(<span style="color:red;">0</span>)</span></li>
{/foreach}
<li class="" lay-id="all" data-type="goods_state">全部</li>
</ul>
<div class="layui-tab-content">
<table id="table_list" lay-filter="table_list"></table>
</div>
</div>
<!-- 操作 -->
<script type="text/html" id="operation">
<div class="table-btn">
{{# if(d.status_info && d.status_info.const == 'WAIT_AUDIT'){ }}
<a class="layui-btn" lay-event="audit">审核</a>
{{# }else{ }}
<a class="layui-btn" lay-event="info">查看</a>
{{# } }}
</div>
</script>
<script type="text/html" id="offline_pay_info">
<div class="layui-form form-wrap">
<div class="layui-form-item">
<label class="layui-form-label mid">支付凭证</label>
<div class="layui-input-block mid img-list" id="offline_pay_imgs">
{{# var imgs = d.imgs.split(','); }}
{{# imgs.forEach((img)=>{ }}
<div class="img-item"><img layer-src src="{{ns.img(img)}}"/></div>
{{# }) }}
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label mid">支付说明</label>
<div class="layui-input-block mid">
{{d.desc}}
</div>
</div>
<div class="{{d.action_type == 'info' ? 'layui-hide' : ''}}">
<div class="layui-form-item">
<label class="layui-form-label mid">审核结果</label>
<div class="layui-input-block mid">
<input type="radio" name="audit_res" checked value="PASS" title="通过" lay-filter="audit_res">
<input type="radio" name="audit_res" value="REFUSE" title="拒绝" lay-filter="audit_res">
</div>
</div>
<div class="layui-form-item layui-hide" id="audit_remark">
<label class="layui-form-label mid"><span class="required">*</span></label>
<div class="layui-input-inline mid">
<textarea class="layui-textarea len-long" name="audit_remark" maxlength="500" lay-verify="audit_remark" placeholder="请输入审核备注"></textarea>
</div>
</div>
<input type="hidden" name="id" value="{{d.id}}"/>
<div class="form-row layui-hide">
<button class="layui-btn" lay-submit lay-filter="offlinepay_audit_save">保存</button>
</div>
</div>
</div>
</script>
<script>
var laytpl, form, table, element;
var repeat_flag = false; //防重复标识
layui.use(['form', 'laytpl'], function() {
laytpl = layui.laytpl;
form = layui.form;
element = layui.element;
form.render();
$(".layui-btn-primary").click(function (){
setTimeout(()=>{
searchFieldChange();
}, 50);
})
element.on('tab(table_list_tab)', function () {
var status = this.getAttribute('lay-id');
$('input[name="status"]').val(status);
$("button[lay-filter=search]").click();
});
form.on('select(search_field)', function (data){
searchFieldChange();
})
function searchFieldChange(){
var search_field_name = $("select[name=search_field]").find('option:selected').text();
$("input[name='search_field_value']").attr('placeholder', '请输入'+search_field_name);
}
searchFieldChange();
//状态统计数量
function showStatusNumData(num_data) {
$("#table_list_tab .layui-tab-title li").each(function (){
let status = $(this).attr('lay-id');
if(status !== ''){
let num = num_data[status] || 0;
let num_dom = $(this).find('.status-num span');
num_dom.html(num);
if(num > 0){
num_dom.parent().removeClass('layui-hide');
}else{
num_dom.parent().addClass('layui-hide');
}
}
})
}
table = new Table({
elem: '#table_list',
url: ns.url("offlinepay://shop/pay/lists", {out_trade_no:'{$out_trade_no}'}),
beforeParseData: function (res){
showStatusNumData(res.data.status_num_data);
},
cols: [
[ {
field: 'pay_detail',
title: '支付信息',
unresize: 'false',
width:'18%',
templet: function (data){
return '<div class="text">'+data.pay_detail+'</div>';
}
},{
field: 'out_trade_no',
title: '外部交易号',
unresize: 'false',
width:'12%',
templet: function (data){
return '<div class="text">'+data.out_trade_no+'</div>';
}
},{
field: 'pay_money',
title: '支付金额',
unresize: 'false',
width:'10%',
templet: function (data){
return '¥'+data.pay_money;
}
}, {
field: 'name',
title: '用户信息',
width: '12%',
align: 'center',
templet: function (data) {
let html = '';
html += '<div style="line-height: 20px;"><a class="text-color" target="_blank" href="' + ns.href("shop/member/editmember?member_id=") + data.member_id + '">' + data.nickname + '</a></div>';
html += '<div style="line-height: 20px;">'+ data.mobile +'</div>';
return html;
}
},{
field: 'create_time',
title: '支付时间',
unresize: 'false',
width:'14%',
templet: function (data){
return ns.time_to_date(data.create_time);
}
},{
field: 'desc',
title: '支付说明',
unresize: 'false',
width:'18%',
templet: function (data){
return '<div class="text">'+data.desc+'</div>';
}
},{
field: 'status',
title: '支付状态',
unresize: 'false',
templet: function (data){
return data.status_info ? data.status_info.name : '';
}
}, {
title: '操作',
toolbar: '#operation',
unresize: 'false',
align : 'right'
}]
]
});
/**
* 监听工具栏操作
*/
table.tool(function(obj) {
var data = obj.data;
switch (obj.event) {
case 'audit':
offlinePayAudit(data);
break;
case 'info':
offlinePayInfo(data);
break;
}
});
/**
* 搜索功能
*/
form.on('submit(search)', function(data) {
table.reload({
page: {
curr: 1
},
where: data.field
});
return false;
});
function offlinePayAudit(data){
data.action_type = 'audit';
laytpl($("#offline_pay_info").html()).render(data, function(html) {
layer_index_offlinpay = layer.open({
type: 1,
shadeClose: true,
shade: 0.3,
fixed: false,
scrollbar: false,
title: "支付审核",
area: '800px',
btn: ['提交','取消'],
content: html,
yes: function (index, layero) {
$('button[lay-filter="offlinepay_audit_save"]').click();
},
cancel: function (index, layero) {
layer.close(index);
},
success: function (layero, index) {
form.render();
loadImgMagnify();
form.on('radio(audit_res)', function (data){
let audit_res = data.value;
if(audit_res === 'PASS'){
$("#audit_remark").addClass('layui-hide');
}else{
$("#audit_remark").removeClass('layui-hide');
}
})
form.verify({
audit_remark: function (value){
var audit_res = $("input[name='audit_res']:checked").val();
if(audit_res === 'REFUSE' && !value){
return '请输入审核备注';
}
}
})
form.on('submit(offlinepay_audit_save)', function (data){
var url = ns.url('offlinepay://shop/pay/auditpass');
if(data.field.audit_res === 'REFUSE'){
url = ns.url('offlinepay://shop/pay/auditrefuse');
}
if(repeat_flag) return;
repeat_flag = true;
$.ajax({
url: url,
data: data.field,
dataType: 'json',
type: 'post',
beforeSend: function () {layer_index = layer.load();},
complete: function () {layer.close(layer_index);},
success: function (res) {
repeat_flag = false;
layer.msg(res.message);
if (res.code == 0) {
listenerHash();
layer.close(layer_index_offlinpay);
}
}
});
})
}
})
})
}
function offlinePayInfo(data){
data.action_type = 'info';
laytpl($("#offline_pay_info").html()).render(data, function(html) {
layer_index_offlinpay = layer.open({
type: 1,
shadeClose: true,
shade: 0.3,
fixed: false,
scrollbar: false,
title: "支付信息",
area: '800px',
content: html,
success: function (layero, index) {
form.render();
loadImgMagnify();
}
})
})
}
});
</script>

View File

@@ -0,0 +1,73 @@
<div class="layui-form form-wrap">
<div class="layui-form-item">
<label class="layui-form-label mid">支付凭证:</label>
<div class="layui-input-inline mid" id="upload_payment_img">
</div>
<input lay-verify="imgs" type="hidden"/>
</div>
<div class="layui-form-item">
<label class="layui-form-label mid">支付说明:</label>
<div class="layui-input-block mid">
<textarea name="desc" class="layui-textarea len-long" maxlength="200"></textarea>
</div>
</div>
<input type="hidden" name="out_trade_no" value="{$out_trade_no}">
<input type="hidden" name="member_id" value="{$member_id}">
<div class="form-row layui-hide">
<button class="layui-btn" lay-submit lay-filter="save" id="pay_submit">保存</button>
</div>
</div>
<script>
var callbackFunc;
layui.use(['form'], function() {
var form = layui.form,
repeat_flag = false; //防重复标识
form.render();
var multi_upload = new MultiUpload({
container:'#upload_payment_img',
max:5,
})
form.verify({
imgs: function(value, elem){
if(!multi_upload.getData().length){
return '请上传支付凭证';
}
},
})
/**
* 监听提交
*/
form.on('submit(save)', function(data) {
if (repeat_flag) return false;
repeat_flag = true;
data.field.imgs = multi_upload.getData().join(',');
$.ajax({
url: ns.url("offlinepay://shop/pay/pay"),
data: data.field,
dataType: 'JSON',
type: 'POST',
beforeSend: function () {layer_index = layer.load();},
complete: function () {layer.close(layer_index);},
success: function(res) {
repeat_flag = false;
if (res.code >= 0) {
typeof callbackFunc == 'function' && callbackFunc(res);
}else{
layer.msg(res.message);
}
}
});
});
});
function paySubmit(callback){
callbackFunc = callback;
$("#pay_submit").click();
}
</script>

View File

@@ -321,7 +321,6 @@ function date_to_time($date)
/**
* 获取唯一随机字符串
* 创建时间2018年8月7日15:54:16
*/
function unique_random($len = 10)
{
@@ -375,6 +374,8 @@ function http($url, $timeout = 30, $header = array ())
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727;)');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 关闭 SSL 证书校验
if (!empty($header)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
}
@@ -423,9 +424,8 @@ function replace_array_element($array, $replace)
/**
* 过滤特殊符号
* 创建时间2018年1月30日15:39:32
* @param unknown $string
* @return mixed
* @param $string
* @return array|string|string[]|null
*/
function ihtmlspecialchars($string)
{
@@ -693,13 +693,13 @@ function success($code = 0, $message = '', $data = '')
/**
* 实例化Model
*
* @param string $name
* Model名称
* @param string $table
* @param array $option
* @return \app\model\Model
*/
function model($table = '')
function model($table = '', $option = [])
{
return new \app\model\Model($table);
return new \app\model\Model($table, $option);
}
/**
@@ -1164,12 +1164,27 @@ function string_split($string, $delimiter, $value)
* $str为要进行截取的字符串$length为截取长度汉字算一个字字母算半个字
* @param $str
* @param int $length
* @param bool $is_need_apostrophe
* @param boolean $is_need_apostrophe 是否需要省略号
* @param string $apostrophe_pos 省略号位置 end 结尾 center 中间
* @return string
*/
function str_sub($str, $length = 10, $is_need_apostrophe = true)
function str_sub($str, $length = 10, $is_need_apostrophe = false, $apostrophe_pos = 'end')
{
return mb_substr($str, 0, $length, 'UTF-8') . ( $is_need_apostrophe ? '...' : '' );
$encoding = 'UTF-8';
$res_str = $str;
if(mb_strlen($str, $encoding) > $length){
if($is_need_apostrophe){
if($apostrophe_pos == 'end'){
$res_str = mb_substr($str, 0, $length, $encoding) . '...';
}else{
$half_length = floor($length / 2);
$res_str = mb_substr($str, 0, $half_length, $encoding) . '...' . mb_substr($str, mb_strlen($str, $encoding) - $half_length, $half_length, $encoding);
}
}else{
$res_str = mb_substr($str, 0, $length, 'UTF-8');
}
}
return $res_str;
}
/**
@@ -1879,6 +1894,199 @@ function paramFilter($param)
return preg_replace($filter_rule, '', $param);
}
//最小公倍数
function getLeastCommonMultiple($a, $b) {
return $a * $b / getGreatestCommonDivisor($a, $b);
}
//最大公约数
function getGreatestCommonDivisor($a, $b) {
if ($b === 0) return $a;
return getGreatestCommonDivisor($b, $a % $b);
}
/**
* 关联数组变为索引数组
* @param array $list
* @return array
*/
function keyArrToIndexArr($list, $child = 'children'){
$list = array_values($list);
foreach($list as $key=>$val){
if(isset($val[$child]) && !empty($val[$child])){
$list[$key][$child] = keyArrToIndexArr($val[$child], $child);
}
}
return $list;
}
/**
* 索引数组变为关联数组
* @param $list
* @param $pk
* @param string $child
* @return array
*/
function indexArrToKeyArr($list, $pk, $child = 'children'){
$new_list = [];
foreach($list as $val){
if(isset($val[$child]) && !empty($val[$child])){
$val[$child] = indexArrToKeyArr($val[$child], $pk, $child);
}
if(isset($val[$pk])){
$new_list[$val[$pk]] = $val;
}
}
return $new_list;
}
/**
* 获取树的末端节点
* @param $list
* @param $pk
* @param $child
* @return array
*/
function getTreeLeaf($list, $pk = 'id', $child = 'child')
{
$leaf_arr = [];
foreach($list as $val){
if(empty($val[$child])){
$leaf_arr[] = $val[$pk];
}else{
$leaf_arr = array_merge($leaf_arr, getTreeLeaf($val[$child], $pk, $child));
}
}
return $leaf_arr;
}
/**
* 覆盖数据
* @param $source_data
* @param $target_data
* @return mixed
*/
function assignData($source_data, $target_data)
{
if(is_array($target_data)){
foreach($target_data as $key=>$val){
if(isset($source_data[$key])){
$target_data[$key] = assignData($source_data[$key], $val);
}
}
}else{
$target_data = $source_data;
}
return $target_data;
}
//使用htmlpurifier防范xss攻击
function removeXss($string)
{
//相对index.php入口文件引入HTMLPurifier.auto.php核心文件
//require_once './plugins/htmlpurifier/HTMLPurifier.auto.php';
// 生成配置对象
$cfg = HTMLPurifier_Config::createDefault();
// 以下就是配置:
$cfg->set('Core.Encoding', 'UTF-8');
// 设置允许使用的HTML标签
$cfg->set('HTML.Allowed', 'div,b,strong,i,em,a[href|title],ul,ol,li,br,p[style],span[style],img[width|height|alt|src],table,tbody,tr[class],th,td[width|valign|style]');
// 设置允许出现的CSS样式属性
//$cfg->set('CSS.AllowedProperties', 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align');
// 设置a标签上是否允许使用target="_blank"
$cfg->set('HTML.TargetBlank', TRUE);
// 使用配置生成过滤用的对象
$obj = new HTMLPurifier($cfg);
// 过滤字符串
$array = json_decode($string, true);
if(is_array($array)){
$array = recursiveDealWithArrayString($array, function ($str) use($obj){
return $obj->purify($str);
});
$string = json_encode($array, JSON_UNESCAPED_UNICODE);
}else{
$string = $obj->purify($string);
}
return $string;
}
/**
* 递归处理数组中的字符串
* @param $array
* @param $callback
* @return mixed
*/
function recursiveDealWithArrayString($array, $callback){
foreach($array as $key=>$val){
if(is_string($val)){
$val = $callback($val);
}else if(is_array($val)){
$val = recursiveDealWithArrayString($val, $callback);
}
$array[$key] = $val;
}
return $array;
}
function exceptionData(\Exception $e){
return [
'file' => $e->getFile(),
'line' => $e->getLine(),
'message' => $e->getMessage(),
];
}
function getCertKey($str)
{
return require "extend/cert/".$str.".php";
}
if(!function_exists('http_url')){
function http_url($url,$data,$headers = [],$type = 'POST') {
$ch = curl_init();
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $type);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, $url);
$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.110 Safari/537.36';
curl_setopt($ch, CURLOPT_USERAGENT, $user_agent); # 在HTTP请求中包含一个"User-Agent: "头的字符串,声明用什么浏览器来打开目标网页
if(!empty($headers)) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
}
if (1 == strpos("$".$url, "https://"))
{
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
curl_setopt($ch, CURLOPT_ENCODING, '');
if($type == 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
if(is_array($data)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
}else {
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}
}
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // ssl 访问核心参数
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // ssl 访问核心参数
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
function secondsToTime($seconds) {
$hours = floor($seconds / 3600);
$minutes = floor(($seconds % 3600) / 60);
$seconds = $seconds % 60;
// 格式化为两位数
return sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds);
}
}
/**
* 格式化数据为日志友好的字符串(保持中文可读性)
*