目录结构
project/
├── app/
│ ├── common.php
│ ├── config/
│ │ └── payment.php
│ ├── controller/
│ │ ├── PaymentController.php
│ │ ├── StoreController.php
│ │ └── AdminController.php
│ ├── model/
│ │ ├── Order.php
│ │ ├── Store.php
│ │ ├── PaymentLog.php
│ │ └── SplitAccount.php
│ ├── service/
│ │ ├── HuifuService.php
│ │ ├── SplitService.php
│ │ └── StoreAccountService.php
│ └── validate/
│ └── PaymentValidate.php
├── database/
│ └── migrations/
│ └── create_split_tables.php
└── vendor/1. 数据库表结构
<?php
// database/migrations/create_split_tables.php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateSplitTables extends Migrator
{
public function change()
{
// 门店分账账户表
$this->table('store_split_account', ['comment' => '门店分账账户'])
->addColumn('store_id', 'integer', ['comment' => '门店ID'])
->addColumn('huifu_merchant_id', 'string', ['limit' => 50, 'comment' => '汇付商户号'])
->addColumn('split_ratio', 'decimal', ['precision' => 5, 'scale' => 2, 'comment' => '分账比例(%)'])
->addColumn('settlement_cycle', 'string', ['limit' => 20, 'comment' => '结算周期:T0/T1/D1'])
->addColumn('bank_account_name', 'string', ['limit' => 100, 'comment' => '银行开户名'])
->addColumn('bank_account_no', 'string', ['limit' => 50, 'comment' => '银行账号'])
->addColumn('bank_name', 'string', ['limit' => 100, 'comment' => '开户银行'])
->addColumn('bank_code', 'string', ['limit' => 20, 'comment' => '银行代码'])
->addColumn('status', 'boolean', ['default' => 1, 'comment' => '状态:0禁用 1启用'])
->addColumn('create_time', 'integer', ['comment' => '创建时间'])
->addColumn('update_time', 'integer', ['comment' => '更新时间'])
->addIndex(['store_id'], ['unique' => true])
->create();
// 订单分账记录表
$this->table('order_split_record', ['comment' => '订单分账记录'])
->addColumn('order_id', 'integer', ['comment' => '订单ID'])
->addColumn('order_no', 'string', ['limit' => 50, 'comment' => '订单号'])
->addColumn('store_id', 'integer', ['comment' => '门店ID'])
->addColumn('platform_order_no', 'string', ['limit' => 100, 'comment' => '汇付订单号'])
->addColumn('total_amount', 'decimal', ['precision' => 10, 'scale' => 2, 'comment' => '订单总金额'])
->addColumn('split_amount', 'decimal', ['precision' => 10, 'scale' => 2, 'comment' => '分账金额'])
->addColumn('platform_fee', 'decimal', ['precision' => 10, 'scale' => 2, 'comment' => '平台手续费'])
->addColumn('split_status', 'boolean', ['default' => 0, 'comment' => '分账状态:0待分账 1分账中 2分账成功 3分账失败'])
->addColumn('split_result', 'text', ['null' => true, 'comment' => '分账结果'])
->addColumn('error_msg', 'string', ['limit' => 500, 'null' => true, 'comment' => '错误信息'])
->addColumn('create_time', 'integer', ['comment' => '创建时间'])
->addColumn('update_time', 'integer', ['comment' => '更新时间'])
->addIndex(['order_no'])
->addIndex(['store_id'])
->create();
// 门店手续费记录表
$this->table('store_fee_record', ['comment' => '门店手续费记录'])
->addColumn('store_id', 'integer', ['comment' => '门店ID'])
->addColumn('order_id', 'integer', ['comment' => '订单ID'])
->addColumn('order_no', 'string', ['limit' => 50, 'comment' => '订单号'])
->addColumn('fee_amount', 'decimal', ['precision' => 10, 'scale' => 2, 'comment' => '手续费金额'])
->addColumn('fee_rate', 'decimal', ['precision' => 5, 'scale' => 2, 'comment' => '手续费率(%)'])
->addColumn('create_time', 'integer', ['comment' => '创建时间'])
->addIndex(['store_id'])
->create();
// 汇付支付日志表
$this->table('huifu_payment_log', ['comment' => '汇付支付日志'])
->addColumn('type', 'string', ['limit' => 50, 'comment' => '日志类型:payment/split/refund'])
->addColumn('order_no', 'string', ['limit' => 50, 'comment' => '订单号'])
->addColumn('request_data', 'text', ['comment' => '请求数据'])
->addColumn('response_data', 'text', ['comment' => '响应数据'])
->addColumn('status', 'boolean', ['default' => 0, 'comment' => '状态:0失败 1成功'])
->addColumn('error_code', 'string', ['limit' => 50, 'null' => true, 'comment' => '错误代码'])
->addColumn('error_msg', 'string', ['limit' => 500, 'null' => true, 'comment' => '错误信息'])
->addColumn('create_time', 'integer', ['comment' => '创建时间'])
->addIndex(['type', 'order_no'])
->create();
}
}2. 配置文件
<?php
// app/config/payment.php
return [
// 汇付支付配置
'huifu' => [
'app_id' => env('HUIFU_APP_ID', ''),
'secret_key' => env('HUIFU_SECRET_KEY', ''),
'merchant_id' => env('HUIFU_MERCHANT_ID', ''),
'api_url' => env('HUIFU_API_URL', 'https://api.huifu.com'),
'notify_url' => env('HUIFU_NOTIFY_URL', 'https://yourdomain.com/api/payment/notify'),
'return_url' => env('HUIFU_RETURN_URL', 'https://yourdomain.com/payment/return'),
'split_notify_url' => env('HUIFU_SPLIT_NOTIFY_URL', 'https://yourdomain.com/api/payment/splitNotify'),
'timeout' => 30,
],
// 平台手续费配置
'platform_fee' => [
'default_rate' => 0.6, // 默认手续费率(%)
'min_fee' => 0.01, // 最低手续费
'max_fee' => 1000, // 最高手续费
],
// 分账配置
'split' => [
'auto_split' => true, // 是否自动分账
'split_time' => 30, // 分账延迟时间(分钟),用于确认收货后退款风险
'retry_times' => 3, // 失败重试次数
'retry_interval' => 5, // 重试间隔(分钟)
],
];3. 核心服务类 - 汇付支付对接
<?php
// app/service/HuifuService.php
namespace app\service;
use think\facade\Log;
use think\facade\Cache;
use app\model\HuifuPaymentLog;
class HuifuService
{
private $appId;
private $secretKey;
private $merchantId;
private $apiUrl;
private $notifyUrl;
private $splitNotifyUrl;
private $timeout;
public function __construct()
{
$config = config('payment.huifu');
$this->appId = $config['app_id'];
$this->secretKey = $config['secret_key'];
$this->merchantId = $config['merchant_id'];
$this->apiUrl = $config['api_url'];
$this->notifyUrl = $config['notify_url'];
$this->splitNotifyUrl = $config['split_notify_url'];
$this->timeout = $config['timeout'];
}
/**
* 统一下单支付接口
*/
public function unifiedOrder($orderNo, $amount, $subject, $body, $storeId = null)
{
$params = [
'app_id' => $this->appId,
'merchant_id' => $this->merchantId,
'out_trade_no' => $orderNo,
'total_amount' => $this->formatAmount($amount),
'subject' => $subject,
'body' => $body,
'notify_url' => $this->notifyUrl,
'return_url' => config('payment.huifu.return_url'),
'timeout_express' => '30m',
'pay_type' => 'WEIXIN_JSAPI', // 支付方式,可根据前端传入调整
];
// 如果有门店,添加分账标识
if ($storeId) {
$params['split_flag'] = 'Y';
$params['split_notify_url'] = $this->splitNotifyUrl;
}
// 生成签名
$params['sign'] = $this->generateSign($params);
// 记录请求日志
$this->logRequest('unified_order', $orderNo, $params);
// 发送请求
$response = $this->post('/v2/payment/unifiedOrder', $params);
// 记录响应日志
$this->logResponse('unified_order', $orderNo, $response);
return $response;
}
/**
* 订单分账接口
*/
public function orderSplit($orderNo, $totalAmount, $splitList)
{
$params = [
'app_id' => $this->appId,
'merchant_id' => $this->merchantId,
'out_trade_no' => $orderNo,
'total_amount' => $this->formatAmount($totalAmount),
'split_list' => $splitList,
];
$params['sign'] = $this->generateSign($params);
$this->logRequest('order_split', $orderNo, $params);
$response = $this->post('/v2/payment/split', $params);
$this->logResponse('order_split', $orderNo, $response);
return $response;
}
/**
* 分账查询接口
*/
public function querySplit($orderNo)
{
$params = [
'app_id' => $this->appId,
'merchant_id' => $this->merchantId,
'out_trade_no' => $orderNo,
];
$params['sign'] = $this->generateSign($params);
$response = $this->post('/v2/payment/querySplit', $params);
return $response;
}
/**
* 退款接口
*/
public function refund($orderNo, $refundNo, $refundAmount, $totalAmount)
{
$params = [
'app_id' => $this->appId,
'merchant_id' => $this->merchantId,
'out_trade_no' => $orderNo,
'out_refund_no' => $refundNo,
'refund_amount' => $this->formatAmount($refundAmount),
'total_amount' => $this->formatAmount($totalAmount),
];
$params['sign'] = $this->generateSign($params);
$this->logRequest('refund', $orderNo, $params);
$response = $this->post('/v2/payment/refund', $params);
$this->logResponse('refund', $orderNo, $response);
return $response;
}
/**
* 订单查询接口
*/
public function queryOrder($orderNo)
{
$params = [
'app_id' => $this->appId,
'merchant_id' => $this->merchantId,
'out_trade_no' => $orderNo,
];
$params['sign'] = $this->generateSign($params);
$response = $this->post('/v2/payment/query', $params);
return $response;
}
/**
* 生成门店分账列表
*/
public function buildSplitList($storeSplits)
{
$splitList = [];
foreach ($storeSplits as $split) {
$splitList[] = [
'merchant_id' => $split['huifu_merchant_id'],
'amount' => $this->formatAmount($split['amount']),
'ratio' => $split['ratio'] ?? null,
'desc' => $split['desc'] ?? '商品销售分账',
];
}
return $splitList;
}
/**
* 验证回调签名
*/
public function verifyNotifySign($params)
{
if (!isset($params['sign'])) {
return false;
}
$sign = $params['sign'];
unset($params['sign']);
$calculatedSign = $this->generateSign($params);
return $sign === $calculatedSign;
}
/**
* 生成签名
*/
private function generateSign($params)
{
// 按照ASCII码排序
ksort($params);
$str = '';
foreach ($params as $key => $value) {
if ($value !== '' && $value !== null && $key !== 'sign') {
$str .= $key . '=' . $value . '&';
}
}
$str = rtrim($str, '&');
$str .= '&key=' . $this->secretKey;
return strtoupper(md5($str));
}
/**
* 发送POST请求
*/
private function post($path, $params)
{
$url = $this->apiUrl . $path;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json',
]);
$response = curl_exec($ch);
$error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($error) {
Log::error('汇付支付请求失败: ' . $error);
return ['code' => 'ERROR', 'msg' => '网络请求失败: ' . $error];
}
if ($httpCode != 200) {
Log::error('汇付支付HTTP错误: ' . $httpCode);
return ['code' => 'ERROR', 'msg' => 'HTTP错误: ' . $httpCode];
}
$result = json_decode($response, true);
if (!$result) {
return ['code' => 'ERROR', 'msg' => '响应解析失败'];
}
return $result;
}
/**
* 格式化金额(分转元,保留2位小数)
*/
private function formatAmount($amount)
{
return number_format($amount / 100, 2, '.', '');
}
/**
* 记录请求日志
*/
private function logRequest($type, $orderNo, $params)
{
try {
HuifuPaymentLog::create([
'type' => $type,
'order_no' => $orderNo,
'request_data' => json_encode($params, JSON_UNESCAPED_UNICODE),
'create_time' => time(),
]);
} catch (\Exception $e) {
Log::error('记录请求日志失败: ' . $e->getMessage());
}
}
/**
* 记录响应日志
*/
private function logResponse($type, $orderNo, $response)
{
try {
$log = HuifuPaymentLog::where('type', $type)
->where('order_no', $orderNo)
->order('id', 'desc')
->find();
if ($log) {
$log->response_data = json_encode($response, JSON_UNESCAPED_UNICODE);
$log->status = isset($response['code']) && $response['code'] == 'SUCCESS' ? 1 : 0;
if (!$log->status) {
$log->error_code = $response['code'] ?? '';
$log->error_msg = $response['msg'] ?? '';
}
$log->save();
}
} catch (\Exception $e) {
Log::error('记录响应日志失败: ' . $e->getMessage());
}
}
}4. 分账服务类
<?php
// app/service/SplitService.php
namespace app\service;
use think\facade\Log;
use think\facade\Db;
use app\model\Order;
use app\model\Store;
use app\model\OrderSplitRecord;
use app\model\StoreSplitAccount;
use app\model\StoreFeeRecord;
use app\service\HuifuService;
class SplitService
{
private $huifuService;
public function __construct()
{
$this->huifuService = new HuifuService();
}
/**
* 处理订单分账
*/
public function processOrderSplit($orderId)
{
Db::startTrans();
try {
$order = Order::with(['orderGoods', 'store'])->find($orderId);
if (!$order || $order['pay_status'] != 1) {
throw new \Exception('订单不存在或未支付');
}
// 检查是否已经分账
$exists = OrderSplitRecord::where('order_id', $orderId)
->whereIn('split_status', [1, 2])
->find();
if ($exists) {
throw new \Exception('订单已处理分账');
}
// 获取门店分账配置
$storeSplits = $this->getStoreSplits($order['order_goods']);
if (empty($storeSplits)) {
// 没有门店需要分账,直接标记为已完成
$this->markOrderSplitComplete($orderId);
Db::commit();
return true;
}
// 计算各门店分账金额
$splitList = $this->calculateStoreSplit($order, $storeSplits);
// 计算平台手续费
$platformFee = $this->calculatePlatformFee($order, $storeSplits);
// 创建分账记录
$splitRecord = $this->createSplitRecord($order, $splitList, $platformFee);
// 如果不需要自动分账,则暂不处理
if (!config('payment.split.auto_split')) {
Db::commit();
return true;
}
// 判断是否需要延迟分账
$needDelay = $this->needDelaySplit($order);
if ($needDelay) {
// 加入延迟队列
$this->addToSplitQueue($orderId, config('payment.split.split_time'));
Db::commit();
return true;
}
// 立即执行分账
$result = $this->executeSplit($order, $splitList, $splitRecord);
Db::commit();
return $result;
} catch (\Exception $e) {
Db::rollback();
Log::error('处理订单分账失败: ' . $e->getMessage() . ',订单ID:' . $orderId);
return false;
}
}
/**
* 执行分账
*/
public function executeSplit($order, $splitList, $splitRecord = null)
{
try {
// 构建分账列表
$huifuSplitList = $this->huifuService->buildSplitList($splitList);
// 调用汇付分账接口
$response = $this->huifuService->orderSplit(
$order['order_no'],
$order['pay_amount'],
$huifuSplitList
);
if (!$splitRecord) {
$splitRecord = OrderSplitRecord::where('order_id', $order['id'])->find();
}
if ($response['code'] == 'SUCCESS') {
// 分账成功
$splitRecord->split_status = 2;
$splitRecord->split_result = json_encode($response);
$splitRecord->update_time = time();
$splitRecord->save();
// 记录分账成功日志
Log::info('订单分账成功:' . $order['order_no']);
return true;
} else {
// 分账失败
$splitRecord->split_status = 3;
$splitRecord->error_msg = $response['msg'] ?? '分账失败';
$splitRecord->update_time = time();
$splitRecord->save();
// 加入重试队列
$this->addToRetryQueue($order['id']);
Log::error('订单分账失败:' . $order['order_no'] . ',错误:' . ($response['msg'] ?? ''));
return false;
}
} catch (\Exception $e) {
Log::error('执行分账异常:' . $e->getMessage());
return false;
}
}
/**
* 获取门店分账配置
*/
private function getStoreSplits($orderGoods)
{
$storeIds = array_unique(array_column($orderGoods, 'store_id'));
if (empty($storeIds)) {
return [];
}
$stores = Store::whereIn('id', $storeIds)
->where('status', 1)
->column('*', 'id');
$accounts = StoreSplitAccount::whereIn('store_id', $storeIds)
->where('status', 1)
->column('*', 'store_id');
$result = [];
foreach ($orderGoods as $goods) {
$storeId = $goods['store_id'];
if (!isset($result[$storeId])) {
$result[$storeId] = [
'store_id' => $storeId,
'store_name' => $stores[$storeId]['name'] ?? '',
'huifu_merchant_id' => $accounts[$storeId]['huifu_merchant_id'] ?? '',
'split_ratio' => $accounts[$storeId]['split_ratio'] ?? 100,
'goods_amount' => 0,
];
}
$result[$storeId]['goods_amount'] += $goods['pay_price'] * $goods['goods_num'];
}
return $result;
}
/**
* 计算门店分账金额
*/
private function calculateStoreSplit($order, $storeSplits)
{
$totalGoodsAmount = array_sum(array_column($storeSplits, 'goods_amount'));
$splitList = [];
foreach ($storeSplits as $storeId => $store) {
// 按商品金额比例分配订单实际支付金额
$ratio = $store['goods_amount'] / $totalGoodsAmount;
$storeAmount = bcmul($order['pay_amount'], $ratio, 2);
// 应用门店分账比例
$splitAmount = bcmul($storeAmount, bcdiv($store['split_ratio'], 100, 4), 2);
$splitList[] = [
'merchant_id' => $store['huifu_merchant_id'],
'store_id' => $storeId,
'amount' => bcmul($splitAmount, 100, 0), // 转为分
'ratio' => $store['split_ratio'],
'desc' => $store['store_name'] . '商品销售分账',
];
// 记录手续费
$feeAmount = bcsub($storeAmount, $splitAmount, 2);
if ($feeAmount > 0) {
$this->recordStoreFee($storeId, $order['id'], $order['order_no'], $feeAmount, $store['split_ratio']);
}
}
return $splitList;
}
/**
* 计算平台手续费
*/
private function calculatePlatformFee($order, $storeSplits)
{
$totalPlatformFee = 0;
$defaultRate = config('payment.platform_fee.default_rate');
foreach ($storeSplits as $store) {
// 获取门店手续费率,可配置不同门店不同费率
$feeRate = $store['split_ratio'] ?? $defaultRate;
$fee = bcmul($store['goods_amount'], bcdiv($feeRate, 100, 4), 2);
// 手续费限制
$minFee = config('payment.platform_fee.min_fee');
$maxFee = config('payment.platform_fee.max_fee');
if ($fee < $minFee) {
$fee = $minFee;
}
if ($fee > $maxFee) {
$fee = $maxFee;
}
$totalPlatformFee = bcadd($totalPlatformFee, $fee, 2);
}
return $totalPlatformFee;
}
/**
* 创建分账记录
*/
private function createSplitRecord($order, $splitList, $platformFee)
{
$record = OrderSplitRecord::create([
'order_id' => $order['id'],
'order_no' => $order['order_no'],
'store_id' => $splitList[0]['store_id'] ?? 0,
'total_amount' => $order['pay_amount'],
'split_amount' => array_sum(array_column($splitList, 'amount')) / 100,
'platform_fee' => $platformFee,
'split_status' => 0,
'create_time' => time(),
]);
return $record;
}
/**
* 标记订单分账完成(无门店分账情况)
*/
private function markOrderSplitComplete($orderId)
{
OrderSplitRecord::create([
'order_id' => $orderId,
'split_status' => 2,
'create_time' => time(),
'update_time' => time(),
]);
}
/**
* 判断是否需要延迟分账
*/
private function needDelaySplit($order)
{
// 判断是否是虚拟商品、是否已完成等
if ($order['order_type'] == 'virtual') {
return false; // 虚拟商品即时分账
}
return true; // 实物商品延迟分账
}
/**
* 记录门店手续费
*/
private function recordStoreFee($storeId, $orderId, $orderNo, $feeAmount, $feeRate)
{
StoreFeeRecord::create([
'store_id' => $storeId,
'order_id' => $orderId,
'order_no' => $orderNo,
'fee_amount' => $feeAmount,
'fee_rate' => $feeRate,
'create_time' => time(),
]);
}
/**
* 加入分账队列
*/
private function addToSplitQueue($orderId, $delayMinutes)
{
$key = 'split_queue_' . $orderId;
$data = [
'order_id' => $orderId,
'execute_time' => time() + $delayMinutes * 60,
];
Cache::set($key, $data, $delayMinutes * 60 + 60);
}
/**
* 加入重试队列
*/
private function addToRetryQueue($orderId)
{
$key = 'split_retry_' . $orderId;
$retryCount = Cache::get($key . '_count', 0);
if ($retryCount >= config('payment.split.retry_times')) {
Log::error('订单分账重试次数已达上限:' . $orderId);
return false;
}
$retryCount++;
$delayMinutes = config('payment.split.retry_interval');
$data = [
'order_id' => $orderId,
'retry_count' => $retryCount,
'execute_time' => time() + $delayMinutes * 60,
];
Cache::set($key, $data, $delayMinutes * 60 + 60);
Cache::set($key . '_count', $retryCount, 86400);
return true;
}
/**
* 处理退款分账
*/
public function processRefundSplit($orderId, $refundAmount)
{
Db::startTrans();
try {
$splitRecord = OrderSplitRecord::where('order_id', $orderId)
->where('split_status', 2)
->find();
if (!$splitRecord) {
throw new \Exception('未找到分账记录');
}
// 更新分账记录,标记为退款中
$splitRecord->split_status = 4; // 自定义状态:退款中
$splitRecord->save();
// 记录退款日志
Log::info('订单退款分账处理:' . $orderId . ',退款金额:' . $refundAmount);
Db::commit();
return true;
} catch (\Exception $e) {
Db::rollback();
Log::error('处理退款分账失败:' . $e->getMessage());
return false;
}
}
}5. 门店账户服务类
<?php
// app/service/StoreAccountService.php
namespace app\service;
use think\facade\Db;
use app\model\Store;
use app\model\StoreSplitAccount;
class StoreAccountService
{
/**
* 创建门店分账账户
*/
public function createStoreAccount($storeId, $data)
{
return Db::transaction(function () use ($storeId, $data) {
// 检查是否已存在
$exists = StoreSplitAccount::where('store_id', $storeId)->find();
if ($exists) {
throw new \Exception('门店分账账户已存在');
}
// 验证银行信息
$this->validateBankInfo($data);
// 创建账户
$account = StoreSplitAccount::create([
'store_id' => $storeId,
'huifu_merchant_id' => $data['huifu_merchant_id'],
'split_ratio' => $data['split_ratio'] ?? 100,
'settlement_cycle' => $data['settlement_cycle'] ?? 'T1',
'bank_account_name' => $data['bank_account_name'],
'bank_account_no' => $data['bank_account_no'],
'bank_name' => $data['bank_name'],
'bank_code' => $data['bank_code'],
'status' => $data['status'] ?? 1,
'create_time' => time(),
'update_time' => time(),
]);
return $account;
});
}
/**
* 更新门店分账账户
*/
public function updateStoreAccount($storeId, $data)
{
$account = StoreSplitAccount::where('store_id', $storeId)->find();
if (!$account) {
throw new \Exception('门店分账账户不存在');
}
// 验证银行信息
if (isset($data['bank_account_no'])) {
$this->validateBankInfo($data);
}
$data['update_time'] = time();
$account->save($data);
return $account;
}
/**
* 批量导入门店分账账户
*/
public function batchImportAccounts($accounts)
{
$success = 0;
$fail = 0;
$errors = [];
Db::startTrans();
try {
foreach ($accounts as $account) {
try {
// 检查门店是否存在
$store = Store::find($account['store_id']);
if (!$store) {
throw new \Exception('门店不存在:' . $account['store_id']);
}
// 创建或更新
$exists = StoreSplitAccount::where('store_id', $account['store_id'])->find();
$data = [
'huifu_merchant_id' => $account['huifu_merchant_id'],
'split_ratio' => $account['split_ratio'] ?? 100,
'settlement_cycle' => $account['settlement_cycle'] ?? 'T1',
'bank_account_name' => $account['bank_account_name'],
'bank_account_no' => $account['bank_account_no'],
'bank_name' => $account['bank_name'],
'bank_code' => $account['bank_code'],
'status' => $account['status'] ?? 1,
'update_time' => time(),
];
if ($exists) {
$exists->save($data);
} else {
$data['store_id'] = $account['store_id'];
$data['create_time'] = time();
StoreSplitAccount::create($data);
}
$success++;
} catch (\Exception $e) {
$fail++;
$errors[] = '门店ID ' . $account['store_id'] . ' 失败:' . $e->getMessage();
}
}
Db::commit();
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
return [
'success' => $success,
'fail' => $fail,
'errors' => $errors,
];
}
/**
* 验证银行信息
*/
private function validateBankInfo($data)
{
if (empty($data['bank_account_name'])) {
throw new \Exception('银行开户名不能为空');
}
if (empty($data['bank_account_no'])) {
throw new \Exception('银行账号不能为空');
}
if (empty($data['bank_name'])) {
throw new \Exception('开户银行不能为空');
}
// 验证银行卡号格式(简单验证)
if (!preg_match('/^\d{16,19}$/', $data['bank_account_no'])) {
throw new \Exception('银行卡号格式不正确');
}
}
/**
* 获取门店分账统计
*/
public function getStoreSplitStats($storeId, $startDate, $endDate)
{
$stats = Db::name('order_split_record')
->where('store_id', $storeId)
->whereBetweenTime('create_time', strtotime($startDate), strtotime($endDate))
->field([
'COUNT(*) as total_count',
'SUM(split_amount) as total_split_amount',
'SUM(platform_fee) as total_fee',
'SUM(CASE WHEN split_status = 2 THEN split_amount ELSE 0 END) as success_amount',
])
->find();
return $stats;
}
}6. 支付控制器
<?php
// app/controller/PaymentController.php
namespace app\controller;
use think\Request;
use think\facade\Log;
use app\BaseController;
use app\model\Order;
use app\model\PaymentLog;
use app\service\HuifuService;
use app\service\SplitService;
class PaymentController extends BaseController
{
private $huifuService;
private $splitService;
public function __construct()
{
$this->huifuService = new HuifuService();
$this->splitService = new SplitService();
}
/**
* 统一下单
*/
public function unifiedOrder(Request $request)
{
$orderNo = $request->param('order_no');
$payType = $request->param('pay_type', 'WEIXIN_JSAPI');
$order = Order::with('store')->where('order_no', $orderNo)->find();
if (!$order) {
return json(['code' => 1, 'msg' => '订单不存在']);
}
if ($order['pay_status'] == 1) {
return json(['code' => 1, 'msg' => '订单已支付']);
}
// 调用汇付统一下单
$response = $this->huifuService->unifiedOrder(
$order['order_no'],
$order['pay_amount'],
$order['order_name'],
$order['order_desc'],
$order['store_id']
);
if ($response['code'] != 'SUCCESS') {
return json(['code' => 1, 'msg' => '下单失败:' . ($response['msg'] ?? '')]);
}
// 返回支付参数
return json([
'code' => 0,
'msg' => 'success',
'data' => [
'pay_params' => $response['pay_params'],
'order_no' => $order['order_no'],
],
]);
}
/**
* 支付回调通知
*/
public function notify(Request $request)
{
$params = $request->post();
Log::info('汇付支付回调:' . json_encode($params));
// 验证签名
if (!$this->huifuService->verifyNotifySign($params)) {
Log::error('汇付支付回调签名验证失败');
return 'fail';
}
$orderNo = $params['out_trade_no'];
$tradeNo = $params['trade_no'];
$payStatus = $params['trade_status'];
if ($payStatus == 'SUCCESS') {
// 处理订单支付成功
$result = $this->processPaidOrder($orderNo, $tradeNo, $params);
if ($result) {
return 'success';
}
}
return 'fail';
}
/**
* 分账回调通知
*/
public function splitNotify(Request $request)
{
$params = $request->post();
Log::info('汇付分账回调:' . json_encode($params));
// 验证签名
if (!$this->huifuService->verifyNotifySign($params)) {
Log::error('汇付分账回调签名验证失败');
return 'fail';
}
$orderNo = $params['out_trade_no'];
$splitStatus = $params['split_status'];
// 更新分账记录
$splitRecord = OrderSplitRecord::where('order_no', $orderNo)->find();
if ($splitRecord) {
if ($splitStatus == 'SUCCESS') {
$splitRecord->split_status = 2;
} else {
$splitRecord->split_status = 3;
$splitRecord->error_msg = $params['fail_reason'] ?? '分账失败';
}
$splitRecord->split_result = json_encode($params);
$splitRecord->update_time = time();
$splitRecord->save();
}
return 'success';
}
/**
* 处理支付成功订单
*/
private function processPaidOrder($orderNo, $tradeNo, $params)
{
try {
$order = Order::where('order_no', $orderNo)->find();
if (!$order) {
Log::error('订单不存在:' . $orderNo);
return false;
}
if ($order['pay_status'] == 1) {
return true; // 已处理过
}
// 更新订单支付状态
$order->pay_status = 1;
$order->pay_time = time();
$order->trade_no = $tradeNo;
$order->pay_info = json_encode($params);
$order->save();
// 触发支付成功事件
event('OrderPaid', $order);
// 处理分账
if ($order['store_id'] > 0) {
// 延迟处理分账,等待订单完成
$this->splitService->processOrderSplit($order['id']);
}
return true;
} catch (\Exception $e) {
Log::error('处理支付成功订单失败:' . $e->getMessage());
return false;
}
}
/**
* 退款申请
*/
public function refund(Request $request)
{
$orderNo = $request->param('order_no');
$refundAmount = $request->param('refund_amount');
$refundReason = $request->param('refund_reason');
$order = Order::where('order_no', $orderNo)->find();
if (!$order) {
return json(['code' => 1, 'msg' => '订单不存在']);
}
if ($order['pay_status'] != 1) {
return json(['code' => 1, 'msg' => '订单未支付']);
}
if ($refundAmount > $order['pay_amount']) {
return json(['code' => 1, 'msg' => '退款金额不能大于支付金额']);
}
// 生成退款单号
$refundNo = 'REFUND' . date('YmdHis') . rand(1000, 9999);
// 调用汇付退款接口
$response = $this->huifuService->refund(
$orderNo,
$refundNo,
$refundAmount,
$order['pay_amount']
);
if ($response['code'] == 'SUCCESS') {
// 更新订单退款状态
$order->refund_status = 1;
$order->refund_amount = $refundAmount;
$order->refund_time = time();
$order->save();
// 处理退款分账
$this->splitService->processRefundSplit($order['id'], $refundAmount);
return json(['code' => 0, 'msg' => '退款申请成功']);
} else {
return json(['code' => 1, 'msg' => '退款失败:' . ($response['msg'] ?? '')]);
}
}
/**
* 查询订单
*/
public function queryOrder(Request $request)
{
$orderNo = $request->param('order_no');
$response = $this->huifuService->queryOrder($orderNo);
return json($response);
}
}7. 门店控制器
<?php
// app/controller/StoreController.php
namespace app\controller;
use think\Request;
use app\BaseController;
use app\model\Store;
use app\service\StoreAccountService;
use app\service\SplitService;
class StoreController extends BaseController
{
private $storeAccountService;
private $splitService;
public function __construct()
{
$this->storeAccountService = new StoreAccountService();
$this->splitService = new SplitService();
}
/**
* 创建门店分账账户
*/
public function createSplitAccount(Request $request)
{
$storeId = $request->param('store_id');
$data = $request->only([
'huifu_merchant_id',
'split_ratio',
'settlement_cycle',
'bank_account_name',
'bank_account_no',
'bank_name',
'bank_code',
]);
try {
$account = $this->storeAccountService->createStoreAccount($storeId, $data);
return json([
'code' => 0,
'msg' => '创建成功',
'data' => $account,
]);
} catch (\Exception $e) {
return json(['code' => 1, 'msg' => $e->getMessage()]);
}
}
/**
* 更新门店分账账户
*/
public function updateSplitAccount(Request $request)
{
$storeId = $request->param('store_id');
$data = $request->only([
'huifu_merchant_id',
'split_ratio',
'settlement_cycle',
'bank_account_name',
'bank_account_no',
'bank_name',
'bank_code',
'status',
]);
try {
$account = $this->storeAccountService->updateStoreAccount($storeId, $data);
return json([
'code' => 0,
'msg' => '更新成功',
'data' => $account,
]);
} catch (\Exception $e) {
return json(['code' => 1, 'msg' => $e->getMessage()]);
}
}
/**
* 获取门店分账账户
*/
public function getSplitAccount(Request $request)
{
$storeId = $request->param('store_id');
$account = \app\model\StoreSplitAccount::where('store_id', $storeId)->find();
return json([
'code' => 0,
'data' => $account,
]);
}
/**
* 门店分账统计
*/
public function splitStats(Request $request)
{
$storeId = $request->param('store_id');
$startDate = $request->param('start_date', date('Y-m-d', strtotime('-30 days')));
$endDate = $request->param('end_date', date('Y-m-d'));
$stats = $this->storeAccountService->getStoreSplitStats($storeId, $startDate, $endDate);
return json([
'code' => 0,
'data' => $stats,
]);
}
/**
* 批量导入门店分账账户
*/
public function batchImportAccounts(Request $request)
{
$file = $request->file('file');
if (!$file) {
return json(['code' => 1, 'msg' => '请上传文件']);
}
// 解析Excel或CSV文件
$accounts = $this->parseImportFile($file);
if (empty($accounts)) {
return json(['code' => 1, 'msg' => '文件内容为空']);
}
try {
$result = $this->storeAccountService->batchImportAccounts($accounts);
return json([
'code' => 0,
'msg' => '导入完成',
'data' => $result,
]);
} catch (\Exception $e) {
return json(['code' => 1, 'msg' => $e->getMessage()]);
}
}
/**
* 解析导入文件(简化版)
*/
private function parseImportFile($file)
{
// 实际项目中需要实现CSV/Excel解析
// 这里仅作示例
$content = file_get_contents($file->getPathname());
$lines = explode("\n", $content);
$accounts = [];
$isFirst = true;
foreach ($lines as $line) {
if ($isFirst) {
$isFirst = false;
continue; // 跳过表头
}
$data = str_getcsv($line);
if (count($data) >= 7) {
$accounts[] = [
'store_id' => $data[0],
'huifu_merchant_id' => $data[1],
'split_ratio' => $data[2],
'bank_account_name' => $data[3],
'bank_account_no' => $data[4],
'bank_name' => $data[5],
'bank_code' => $data[6],
];
}
}
return $accounts;
}
}8. 后台管理控制器
<?php
// app/controller/AdminController.php
namespace app\controller;
use think\Request;
use think\facade\Db;
use app\BaseController;
use app\model\Order;
use app\model\Store;
use app\model\OrderSplitRecord;
use app\model\StoreSplitAccount;
use app\model\StoreFeeRecord;
use app\service\SplitService;
class AdminController extends BaseController
{
private $splitService;
public function __construct()
{
$this->splitService = new SplitService();
}
/**
* 分账记录列表
*/
public function splitRecordList(Request $request)
{
$page = $request->param('page', 1);
$limit = $request->param('limit', 20);
$storeId = $request->param('store_id');
$orderNo = $request->param('order_no');
$status = $request->param('status');
$query = OrderSplitRecord::with(['store']);
if ($storeId) {
$query->where('store_id', $storeId);
}
if ($orderNo) {
$query->where('order_no', 'like', '%' . $orderNo . '%');
}
if ($status !== '') {
$query->where('split_status', $status);
}
$list = $query->order('id', 'desc')
->paginate($limit, false, ['page' => $page]);
return json([
'code' => 0,
'data' => $list->items(),
'total' => $list->total(),
]);
}
/**
* 分账详情
*/
public function splitDetail(Request $request)
{
$id = $request->param('id');
$record = OrderSplitRecord::with(['store'])->find($id);
if (!$record) {
return json(['code' => 1, 'msg' => '记录不存在']);
}
return json([
'code' => 0,
'data' => $record,
]);
}
/**
* 手动执行分账
*/
public function manualSplit(Request $request)
{
$orderId = $request->param('order_id');
$order = Order::with('orderGoods')->find($orderId);
if (!$order) {
return json(['code' => 1, 'msg' => '订单不存在']);
}
// 调用分账服务
$result = $this->splitService->processOrderSplit($orderId);
if ($result) {
return json(['code' => 0, 'msg' => '分账处理成功']);
} else {
return json(['code' => 1, 'msg' => '分账处理失败']);
}
}
/**
* 分账重试
*/
public function retrySplit(Request $request)
{
$id = $request->param('id');
$record = OrderSplitRecord::find($id);
if (!$record) {
return json(['code' => 1, 'msg' => '记录不存在']);
}
if ($record['split_status'] != 3) {
return json(['code' => 1, 'msg' => '只有失败状态可以重试']);
}
$order = Order::find($record['order_id']);
if (!$order) {
return json(['code' => 1, 'msg' => '订单不存在']);
}
// 获取门店分账配置
$storeSplits = $this->splitService->getStoreSplits($order['order_goods']);
// 构建分账列表
$splitList = $this->splitService->calculateStoreSplit($order, $storeSplits);
// 执行分账
$result = $this->splitService->executeSplit($order, $splitList, $record);
if ($result) {
return json(['code' => 0, 'msg' => '重试成功']);
} else {
return json(['code' => 1, 'msg' => '重试失败']);
}
}
/**
* 门店手续费统计
*/
public function storeFeeStats(Request $request)
{
$storeId = $request->param('store_id');
$startDate = $request->param('start_date', date('Y-m-d', strtotime('-30 days')));
$endDate = $request->param('end_date', date('Y-m-d'));
$page = $request->param('page', 1);
$limit = $request->param('limit', 20);
$query = StoreFeeRecord::with(['store']);
if ($storeId) {
$query->where('store_id', $storeId);
}
if ($startDate) {
$query->where('create_time', '>=', strtotime($startDate));
}
if ($endDate) {
$query->where('create_time', '<=', strtotime($endDate . ' 23:59:59'));
}
$list = $query->order('id', 'desc')
->paginate($limit, false, ['page' => $page]);
// 统计合计
$total = Db::name('store_fee_record')
->where($query->getOptions('where'))
->sum('fee_amount');
return json([
'code' => 0,
'data' => $list->items(),
'total' => $list->total(),
'total_fee' => $total,
]);
}
/**
* 平台分账总览
*/
public function platformSplitOverview(Request $request)
{
$date = $request->param('date', date('Y-m-d'));
$today = strtotime($date);
$tomorrow = $today + 86400;
// 今日分账统计
$todayStats = Db::name('order_split_record')
->whereBetweenTime('create_time', $today, $tomorrow)
->field([
'COUNT(*) as total_orders',
'SUM(split_amount) as total_split',
'SUM(platform_fee) as total_fee',
'SUM(CASE WHEN split_status = 2 THEN split_amount ELSE 0 END) as success_amount',
'COUNT(CASE WHEN split_status = 2 THEN 1 END) as success_count',
'COUNT(CASE WHEN split_status = 3 THEN 1 END) as fail_count',
])
->find();
// 本月统计
$monthStart = strtotime(date('Y-m-01', $today));
$monthStats = Db::name('order_split_record')
->where('create_time', '>=', $monthStart)
->field([
'SUM(split_amount) as total_split',
'SUM(platform_fee) as total_fee',
])
->find();
// 待分账订单
$pendingCount = OrderSplitRecord::where('split_status', 0)
->where('create_time', '<=', time() - 3600)
->count();
// 分账失败订单
$failCount = OrderSplitRecord::where('split_status', 3)->count();
return json([
'code' => 0,
'data' => [
'today' => $todayStats,
'month' => $monthStats,
'pending_count' => $pendingCount,
'fail_count' => $failCount,
],
]);
}
}9. 命令行任务
<?php
// app/command/SplitTask.php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Log;
use think\facade\Cache;
use app\model\Order;
use app\model\OrderSplitRecord;
use app\service\SplitService;
class SplitTask extends Command
{
protected function configure()
{
$this->setName('split:process')
->setDescription('处理待分账订单');
}
protected function execute(Input $input, Output $output)
{
$output->writeln('开始处理待分账订单...');
try {
$splitService = new SplitService();
// 处理延迟分账队列
$this->processDelayQueue($splitService);
// 处理失败重试队列
$this->processRetryQueue($splitService);
// 处理超时未分账订单
$this->processTimeoutOrders($splitService);
$output->writeln('处理完成');
} catch (\Exception $e) {
$output->writeln('处理失败:' . $e->getMessage());
Log::error('分账任务执行失败:' . $e->getMessage());
}
}
/**
* 处理延迟分账队列
*/
private function processDelayQueue($splitService)
{
$keys = Cache::get('split_queue_keys', []);
$now = time();
foreach ($keys as $key) {
$data = Cache::get($key);
if ($data && $data['execute_time'] <= $now) {
$orderId = $data['order_id'];
$order = Order::with('orderGoods')->find($orderId);
if ($order) {
$splitService->processOrderSplit($orderId);
}
Cache::delete($key);
}
}
}
/**
* 处理失败重试队列
*/
private function processRetryQueue($splitService)
{
$keys = Cache::get('split_retry_keys', []);
$now = time();
foreach ($keys as $key) {
$data = Cache::get($key);
if ($data && $data['execute_time'] <= $now) {
$orderId = $data['order_id'];
$record = OrderSplitRecord::where('order_id', $orderId)
->where('split_status', 3)
->find();
if ($record) {
$order = Order::with('orderGoods')->find($orderId);
if ($order) {
// 获取门店分账配置
$storeSplits = $splitService->getStoreSplits($order['order_goods']);
$splitList = $splitService->calculateStoreSplit($order, $storeSplits);
$splitService->executeSplit($order, $splitList, $record);
}
}
Cache::delete($key);
}
}
}
/**
* 处理超时未分账订单
*/
private function processTimeoutOrders($splitService)
{
// 查找超过24小时未分账的订单
$timeout = time() - 86400;
$records = OrderSplitRecord::where('split_status', 0)
->where('create_time', '<=', $timeout)
->select();
foreach ($records as $record) {
$order = Order::with('orderGoods')->find($record['order_id']);
if ($order) {
$splitService->processOrderSplit($order['id']);
}
}
}
}10. 常用辅助函数
<?php
// app/common.php
use think\facade\Log;
/**
* 记录分账日志
*/
function log_split($message, $data = [])
{
$log = [
'time' => date('Y-m-d H:i:s'),
'message' => $message,
'data' => $data,
];
Log::channel('split')->info(json_encode($log, JSON_UNESCAPED_UNICODE));
}
/**
* 生成唯一订单号
*/
function generate_order_no($prefix = '')
{
return $prefix . date('YmdHis') . rand(1000, 9999);
}
/**
* 金额分转元
*/
function amount_fen_to_yuan($amount)
{
return number_format($amount / 100, 2, '.', '');
}
/**
* 金额元转分
*/
function amount_yuan_to_fen($amount)
{
return intval(bcmul($amount, 100, 0));
}
/**
* 计算分账金额
*/
function calculate_split_amount($total, $ratio)
{
return bcmul($total, bcdiv($ratio, 100, 4), 2);
}11. 数据库迁移执行
# 执行数据库迁移
php think migrate:run12. 添加路由配置
<?php
// route/app.php
use think\facade\Route;
// 支付相关路由
Route::group('payment', function () {
Route::post('unifiedOrder', 'PaymentController/unifiedOrder');
Route::post('notify', 'PaymentController/notify')->withoutMiddleware();
Route::post('splitNotify', 'PaymentController/splitNotify')->withoutMiddleware();
Route::post('refund', 'PaymentController/refund');
Route::get('query', 'PaymentController/queryOrder');
});
// 门店分账相关路由
Route::group('store', function () {
Route::post('splitAccount/create', 'StoreController/createSplitAccount');
Route::post('splitAccount/update', 'StoreController/updateSplitAccount');
Route::get('splitAccount/get', 'StoreController/getSplitAccount');
Route::get('splitStats', 'StoreController/splitStats');
Route::post('splitAccount/batchImport', 'StoreController/batchImportAccounts');
})->middleware('Auth');
// 后台管理路由
Route::group('admin', function () {
Route::get('splitRecord/list', 'AdminController/splitRecordList');
Route::get('splitRecord/detail', 'AdminController/splitDetail');
Route::post('split/manual', 'AdminController/manualSplit');
Route::post('split/retry', 'AdminController/retrySplit');
Route::get('fee/stats', 'AdminController/storeFeeStats');
Route::get('split/overview', 'AdminController/platformSplitOverview');
})->middleware('AdminAuth');13. 配置文件添加日志通道
<?php
// config/log.php
return [
'default' => env('LOG_CHANNEL', 'file'),
'channels' => [
'file' => [
'type' => 'file',
'path' => runtime_path() . '/log/app.log',
'level' => ['debug', 'info', 'error'],
],
'split' => [
'type' => 'file',
'path' => runtime_path() . '/log/split.log',
'level' => ['info', 'error'],
],
'huifu' => [
'type' => 'file',
'path' => runtime_path() . '/log/huifu.log',
'level' => ['info', 'error'],
],
],
];使用说明
- 安装依赖:确保ThinkPHP 6.0+环境
- 配置参数:在.env文件中配置汇付支付相关参数
- 执行迁移:运行数据库迁移创建相关表
- 配置定时任务:设置cron执行分账任务
- 测试对接:使用汇付支付测试环境进行联调

