一、商户注册与配置
- 注册支付平台账号:在拉卡拉开放平台注册商户账号(私信联系注册)
- 创建应用:获取小程序应用ID(AppID)
- 配置支付参数 :
- 商户号(MID)
- 终端号(TID)
- API密钥
- 支付回调地址
二、配置拉卡拉参数(后台)
在app/admin/controller/system/config/PayConfig.php
中添加:
// 文件路径:app/admin/controller/system/config/PayConfig.php
public function index()
{
//...已有代码...
$list = [
// 添加拉卡拉支付配置
[
'menu_name' => '拉卡拉支付',
'config' => [
// 商户编号
[
'type' => 'text',
'name' => 'lakala_merchant_id',
'title' => '商户号(MID)',
],
// 终端号
[
'type' => 'text',
'name' => 'lakala_terminal_id',
'title' => '终端号(TID)',
],
// API密钥
[
'type' => 'text',
'name' => 'lakala_api_key',
'title' => 'API密钥',
],
// 应用ID(小程序)
[
'type' => 'text',
'name' => 'lakala_app_id',
'title' => '小程序AppID',
],
// 是否启用
[
'type' => 'radio',
'name' => 'lakala_status',
'title' => '启用状态',
'value' => 0,
'options' => [
['label' => '关闭', 'value' => 0],
['label' => '开启', 'value' => 1]
]
]
]
]
];
//...后续代码...
}
三、支付服务层(核心)
// 文件路径:app/services/pay/LakalaPayService.php
<?php
namespace app\services\pay;
use think\facade\Config;
use app\services\BaseServices;
use app\services\order\StoreOrderServices;
class LakalaPayService extends BaseServices
{
protected $apiUrl = 'https://api.lakala.com/payment/gateway'; // 正式环境
// protected $apiUrl = 'https://test.api.lakala.com/payment/gateway'; // 测试环境
// 小程序支付下单
public function miniPay($order)
{
$config = $this->getConfig();
if (!$config['status']) throw new \Exception('拉卡拉支付未开启');
$params = [
'version' => '1.0',
'merchant_id' => $config['merchant_id'],
'terminal_id' => $config['terminal_id'],
'biz_type' => 'MINIPRO',
'trade_type' => 'JSAPI',
'notify_url' => sys_config('site_url') . '/api/pay/lakala/notify',
'out_trade_no' => $order['order_id'],
'total_fee' => bcmul($order['pay_price'], 100), // 转为分
'body' => '订单支付',
'sub_appid' => $config['app_id'],
'sub_openid' => $order['openid'], // 小程序用户openid
'attach' => 'store_id:' . $order['store_id'] // 多门店标识
];
// 生成签名
$params['sign'] = $this->generateSign($params, $config['api_key']);
// 请求拉卡拉接口
$result = $this->curlPost($this->apiUrl, $params);
if ($result['return_code'] != 'SUCCESS') {
throw new \Exception('拉卡拉支付请求失败: ' . $result['return_msg']);
}
// 返回小程序支付参数
return [
'appId' => $config['app_id'],
'package' => 'prepay_id=' . $result['prepay_id'],
'timeStamp' => (string) time(),
'nonceStr' => get_nonce(16),
'signType' => 'MD5',
'paySign' => $this->generateJsSign($result, $config['api_key'])
];
}
// 生成支付签名
private function generateSign($data, $key)
{
ksort($data);
$string = '';
foreach ($data as $k => $v) {
if ($v === '' || $k == 'sign') continue;
$string .= $k . '=' . $v . '&';
}
$string .= 'key=' . $key;
return strtoupper(md5($string));
}
// 生成JS支付签名
private function generateJsSign($result, $key)
{
$data = [
'appId' => $result['appid'],
'timeStamp' => (string) time(),
'nonceStr' => get_nonce(16),
'package' => 'prepay_id=' . $result['prepay_id'],
'signType' => 'MD5'
];
ksort($data);
$string = implode('&', array_map(function($k, $v) {
return "$k=$v";
}, array_keys($data), $data));
$string .= '&key=' . $key;
return strtoupper(md5($string));
}
// 处理支付回调
public function handleNotify()
{
$xml = file_get_contents('php://input');
$data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
// 验证签名
$sign = $data['sign'];
unset($data['sign']);
if ($sign != $this->generateSign($data, config('pay.lakala_api_key'))) {
return false;
}
// 获取门店ID
$attach = explode(':', $data['attach']);
$storeId = isset($attach[1]) ? intval($attach[1]) : 0;
/** @var StoreOrderServices $orderService */
$orderService = app()->make(StoreOrderServices::class);
return $orderService->successPay($data['out_trade_no'], [
'pay_type' => 'lakala',
'store_id' => $storeId
]);
}
// 获取配置
private function getConfig()
{
return [
'merchant_id' => sys_config('lakala_merchant_id'),
'terminal_id' => sys_config('lakala_terminal_id'),
'api_key' => sys_config('lakala_api_key'),
'app_id' => sys_config('lakala_app_id'),
'status' => sys_config('lakala_status')
];
}
// HTTP POST请求
private function curlPost($url, $data)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
curl_close($ch);
return json_decode(json_encode(simplexml_load_string($response)), true);
}
}
四、支付控制器
// 文件路径:app/api/controller/v1/pay/PayController.php
public function lakalaPay()
{
$orderId = $this->request->param('order_id');
$openid = $this->request->param('openid'); // 小程序获取的openid
// 验证订单
$order = $this->validateOrder($orderId, $openid);
try {
/** @var LakalaPayService $lakala */
$lakala = app()->make(LakalaPayService::class);
$payment = $lakala->miniPay([
'order_id' => $orderId,
'pay_price' => $order['pay_price'],
'openid' => $openid,
'store_id' => $order['store_id']
]);
return $this->success(compact('payment'));
} catch (\Throwable $e) {
return $this->fail($e->getMessage());
}
}
五、小程序端调用
// 小程序端支付调用
wx.request({
url: '/api/pay/lakala',
method: 'POST',
data: {
order_id: '订单ID',
openid: '用户openid'
},
success: (res) => {
const payment = res.data.payment;
wx.requestPayment({
appId: payment.appId,
timeStamp: payment.timeStamp,
nonceStr: payment.nonceStr,
package: payment.package,
signType: payment.signType,
paySign: payment.paySign,
success: () => {
wx.showToast({ title: '支付成功' });
},
fail: (err) => {
wx.showToast({ title: '支付失败', icon: 'error' });
}
});
}
});
六、回调路由设置
// 文件路径:route/app.php
Route::post('api/pay/lakala/notify', 'api/pay.Pay/lakalaNotify');
七、回调控制器
// 文件路径:app/api/controller/pay/Pay.php
public function lakalaNotify()
{
/** @var LakalaPayService $lakala */
$lakala = app()->make(LakalaPayService::class);
try {
$result = $lakala->handleNotify();
if ($result) {
return response('<xml><return_code>SUCCESS</return_code></xml>', 200, [], 'xml');
}
} catch (\Throwable $e) {
Log::error('拉卡拉回调异常:' . $e->getMessage());
}
return response('<xml><return_code>FAIL</return_code></xml>', 200, [], 'xml');
}
配置注意事项:
- 拉卡拉参数:在后台系统中配置商户号、终端号、API密钥和小程序AppID
- 商户证书:如需双向验证,需在CURL请求中添加证书配置
- 多门店处理 :
- 支付请求中附加
store_id
参数 - 回调中解析门店ID并更新对应门店订单
- 支付请求中附加
- 跨域问题:确保API路由支持小程序跨域请求
签名验证流程:
- 所有参数按参数名ASCII码升序排序
- 使用URL键值对格式拼接参数
- 拼接API密钥(
&key=XXX
) - 对结果进行MD5签名(转大写)