// +---------------------------------------------------------------------- namespace App\Services; use App\Models\AccountLogModel; use App\Models\AccountStatisticsModel; use App\Models\OrderModel; use App\Models\PaymentModel; use Illuminate\Support\Facades\DB; use Yansongda\Pay\Pay; use Yansongda\Pay\Plugin\Wechat\Fund\Profitsharing\AddReceiverPlugin; use Yansongda\Pay\Plugin\Wechat\Fund\Profitsharing\CreatePlugin; use Yansongda\Pay\Provider\Wechat; /** * 支付-服务类 * @author laravel开发员 * @since 2020/11/11 * Class PaymentService * @package App\Services\Api */ class PaymentService extends BaseService { protected static $instance = null; private $config = []; /** * 构造函数 * @author laravel开发员 * @since 2020/11/11 * PaymentService constructor. */ public function __construct() { $this->model = new PaymentModel(); } /** * 静态入口 * @return static|null */ public static function make() { if (!self::$instance) { self::$instance = (new static()); } return self::$instance; } /** * 创建支付 * @param string $scene 场景,store-购物消费,pay-生活充值,refund-退款 * @param int $payType * @param int $isMin 是否是小程序 * @return false|\Yansongda\Pay\Provider\Alipay|Wechat */ public function createPay($scene, $payType = 10, $payPt = '') { $config = ConfigService::make()->getConfigOptionByGroup(6); try { if ($payType == 10) { $appid = isset($config['wxpay_appid']) ? $config['wxpay_appid'] : ''; $mpAppid = isset($config['wxpay_mp_appid']) ? $config['wxpay_mp_appid'] : ''; $minAppid = isset($config['wxpay_min_appid']) ? $config['wxpay_min_appid'] : ''; $mchid = isset($config['wxpay_mchd']) ? $config['wxpay_mchd'] : ''; $secretV3Key = isset($config['wxpay_key_v3']) ? $config['wxpay_key_v3'] : ''; $secretV2Key = isset($config['wxpay_key_v2']) ? $config['wxpay_key_v2'] : ''; $wxpaySecretCert = isset($config['wxpay_secret_cert']) ? $config['wxpay_secret_cert'] : ''; $wxpayPublicCert = isset($config['wxpay_public_cert']) ? $config['wxpay_public_cert'] : ''; if (empty($appid) || empty($mchid) || empty($secretV3Key)) { $this->error = 2616; return false; } // 支付参数 $payConfig = config('payment.wechat'); $payConfig['wechat']['default']['mch_id'] = $mchid; if ($payPt == 'min') { // 小程序支付 $payConfig['wechat']['default']['mini_app_id'] = $minAppid ? $minAppid : $appid; } else if ($payPt == 'mp' || $scene == 'sharing') { // 公众号 $payConfig['wechat']['default']['mp_app_id'] = $mpAppid ? $mpAppid : $appid; } else { // APP支付 $payConfig['wechat']['default']['app_id'] = $appid; } if ($secretV3Key) { $payConfig['wechat']['default']['mch_secret_key'] = $secretV3Key; } else if ($secretV2Key) { $payConfig['wechat']['default']['mch_secret_key_v2'] = $secretV2Key; } if ($wxpaySecretCert) { $payConfig['wechat']['default']['mch_secret_cert'] = $wxpaySecretCert; } if ($wxpayPublicCert) { $payConfig['wechat']['default']['mch_public_cert_path'] = $wxpayPublicCert; } //$payConfig['wechat']['default']['notify_url'] = url('/api/notify/' . $scene . '/10'); $payConfig['wechat']['default']['notify_url'] = url(env('APP_URL') . 'api/notify/' . $scene . '/10'); $this->config = $payConfig; //var_dump($payConfig); return Pay::wechat($payConfig); } else if ($payType == 20) { $appid = isset($config['alipay_appid']) ? $config['alipay_appid'] : ''; $appSecretCert = isset($config['alipay_secret_cert']) ? $config['alipay_secret_cert'] : ''; $appPublicCert = isset($config['alipay_app_public_cert_path']) ? $config['alipay_app_public_cert_path'] : ''; $alipayPublicCert = isset($config['alipay_public_cert_path']) ? $config['alipay_public_cert_path'] : ''; $alipayRootCert = isset($config['alipay_root_cert_path']) ? $config['alipay_root_cert_path'] : ''; if (empty($appid) || empty($appSecretCert)) { $this->error = 2619; return false; } // 支付参数 $payConfig = config('payment.alipay'); $payConfig['alipay']['default']['app_id'] = $appid; $payConfig['alipay']['default']['app_secret_cert'] = $appSecretCert; if ($appPublicCert) { $payConfig['alipay']['default']['app_public_cert_path'] = $appPublicCert; } if ($alipayPublicCert) { $payConfig['alipay']['default']['alipay_public_cert_path'] = $alipayPublicCert; } if ($alipayRootCert) { $payConfig['alipay']['default']['alipay_root_cert_path'] = $alipayRootCert; } $payConfig['alipay']['default']['notify_url'] = url('/api/notify/' . $scene . '/20'); $this->config = $payConfig; return Pay::alipay($payConfig); } else if ($payType == 30) { return true; } return false; } catch (\Exception $exception){ $this->errorData = $this->config; $this->error = '请检查支付配置是否正常:'.$exception->getMessage(); return false; } } /** * 微信小程序支付 * @param $userInfo * @param $order * @param string $scene * @return false|\Yansongda\Supports\Collection */ public function minPay($userInfo, $order, $scene = 'store') { $amount = isset($order['pay_money']) ? $order['pay_money'] : 0; $openid = isset($order['openid']) ? $order['openid'] : ''; $isRevenue = isset($order['is_revenue']) ? $order['is_revenue'] : 0; if ($amount < 0) { $this->error = 2615; return false; } if (empty($openid)) { $this->error = 2614; return false; } $outTradeNo = isset($order['order_no']) && $order['order_no'] ? $order['order_no'] : get_order_num('PR'); // 是否调用过支付,是则用新的支付单号 if ($outTradeNo && $this->model->where(['out_trade_no' => $outTradeNo, 'mark' => 1])->value('id')) { $outTradeNo = $outTradeNo . date('s') . rand(1, 9); } $body = isset($order['body']) ? $order['body'] : ''; $payData = [ 'out_trade_no' => $outTradeNo, 'description' => $body ? $body : '订单支付', 'amount' => [ 'total' => intval($amount * 1000/10), 'currency' => 'CNY' ], 'payer' => [ 'openid' => $openid, ], ]; // 订单分账 if($isRevenue==1){ $payData['settle_info'] = [ 'profit_sharing'=> true, ]; } // 创建支付 try { $pay = $this->createPay($scene, 10, 'min'); RedisService::set("caches:payments:wechat:{$scene}_{$outTradeNo}", ['order' => $order, 'config' => $this->config], 7200); if (empty($pay)) { $this->error = 2616; return false; } $pay = $pay->mini($payData); } catch (\Exception $exception) { RedisService::set("caches:payments:wechat:{$scene}_{$outTradeNo}_error", ['order' => $order, 'error' => $exception->getTrace(), 'config' => $this->config], 7200); $this->error = $exception->getMessage(); return false; } if ($pay->package) { $data = [ 'user_id' => $userInfo['id'], 'out_trade_no' => $outTradeNo, 'order_no' => $order['order_no'], 'params' => json_encode($pay, 256), 'total_fee' => $amount, 'pay_type' => 10, 'create_time' => time(), 'status' => 2, 'mark' => 1, ]; if ($this->model->insertGetId($data)) { $this->error = 2617; return $pay; } } $this->error = 2618; return false; } /** * 微信支付 * @param $userInfo * @param $order * @param string $scene * @return false|\Yansongda\Supports\Collection */ public function wechatPay($userInfo, $order, $scene = 'store') { $amount = isset($order['pay_money']) ? $order['pay_money'] : 0; if ($amount < 0) { $this->error = 2615; return false; } $outTradeNo = isset($order['order_no']) && $order['order_no'] ? $order['order_no'] : get_order_num('PR'); // 是否调用过支付,是则用新的支付单号 if ($outTradeNo && $this->model->where(['out_trade_no' => $outTradeNo, 'mark' => 1])->value('id')) { $outTradeNo = $outTradeNo . date('s') . rand(1, 9); } $body = isset($order['body']) ? $order['body'] : ''; $type = isset($order['type']) ? $order['type'] : 0; $payData = [ 'out_trade_no' => $outTradeNo, 'attach' => "order-{$type}", 'description' => $body ? $body : '订单支付', 'amount' => [ 'total' => intval($amount * 1000/10), 'currency' => 'CNY' ], ]; // 创建支付 try { $pay = $this->createPay($scene, 10); RedisService::set("caches:payments:wechat:{$scene}_{$outTradeNo}", ['order' => $order, 'config' => $this->config], 7200); if (empty($pay)) { $this->error = 2616; return false; } $pay = $pay->app($payData); } catch (\Exception $exception) { RedisService::set("caches:payments:wechat:{$scene}_{$outTradeNo}_error", ['order' => $order, 'error' => $exception->getTrace(), 'config' => $this->config], 7200); $this->error = $exception->getMessage(); return false; } if ($pay->prepayid) { $data = [ 'user_id' => $userInfo['id'], 'out_trade_no' => $outTradeNo, 'order_no' => $order['order_no'], 'params' => json_encode($pay, 256), 'total_fee' => $amount, 'pay_type' => 10, 'create_time' => time(), 'status' => 2, 'mark' => 1, ]; if ($this->model->insertGetId($data)) { $this->error = 2617; return $pay; } } $this->error = 2618; return false; } /** * 支付宝支付 * @param $userInfo * @param $order * @return bool */ public function aliPay($userInfo, $order, $scene = 'shop') { $amount = isset($order['pay_money']) ? $order['pay_money'] : 0; if ($amount < 0) { $this->error = 2615; return false; } // 是否调用过支付,是则用新的支付单号 $outTradeNo = isset($order['order_no']) && $order['order_no'] ? $order['order_no'] : get_order_num('PY'); if ($outTradeNo && $this->model->where(['out_trade_no' => $outTradeNo, 'mark' => 1])->value('id')) { $outTradeNo = $outTradeNo . date('s') . rand(1, 9); } $body = isset($order['body']) ? $order['body'] : ''; $payData = [ 'out_trade_no' => $outTradeNo, 'subject' => $body ? $body : '订单支付', 'total_amount' => $amount, ]; // 创建支付 $pay = $this->createPay($scene, 20); RedisService::set("caches:payments:alipay:{$scene}_{$outTradeNo}", ['order' => $order, 'config' => $this->config], 7200); if (empty($pay)) { $this->error = 2619; return false; } $pay = $pay->app($payData); if ($pay->getStatusCode() == 200) { $data = [ 'user_id' => $userInfo['id'], 'out_trade_no' => $outTradeNo, 'order_no' => $order['order_no'], 'params' => json_encode($pay, 256), 'total_fee' => $amount, 'pay_type' => 20, 'create_time' => time(), 'status' => 2, 'mark' => 1, ]; if ($this->model->insertGetId($data)) { $this->error = 2620; return $pay->getBody()->getContents(); } } $this->error = 2621; return false; } /** * 订单支付回调处理 * @param string $scene 场景 store-购物消费,pay-充值,refund-退款 * @param int $payType 支付方式,10-微信支付,20-支付宝支付 * @param array $data 回调数据 * @return bool */ public function catchNotify($scene, $payType, $data) { $outTradeNo = ''; $payTotal = 0; $transactionId = ''; $payAt = ''; $notifyData = []; try { // 微信支付 if ($payType == 10) { $resource = isset($data['resource']) ? $data['resource'] : []; $ciphertext = isset($resource['ciphertext']) ? $resource['ciphertext'] : []; $tradeStatus = isset($ciphertext['trade_state']) ? $ciphertext['trade_state'] : ''; if ($tradeStatus != 'SUCCESS') { $this->error = 2622; return false; } $outTradeNo = isset($ciphertext['out_trade_no']) ? $ciphertext['out_trade_no'] : ''; $transactionId = isset($ciphertext['transaction_id']) ? $ciphertext['transaction_id'] : ''; if (empty($outTradeNo)) { $this->error = 2623; return false; } $payAt = isset($ciphertext['success_time']) ? date('Y-m-d H:i:s', strtotime($ciphertext['success_time'])) : date('Y-m-d H:i:s'); $amount = isset($ciphertext['amount']) ? $ciphertext['amount'] : []; $payTotal = isset($amount['total']) ? moneyFormat($amount['total'] / 100, 3) : 0; $notifyData = $ciphertext; if ($payTotal <= 0) { $this->error = 2624; return false; } } // 支付宝支付 else if ($payType == 20) { // TRADE_SUCCESS $tradeStatus = isset($data['trade_status']) ? $data['trade_status'] : ''; if ($tradeStatus != 'TRADE_SUCCESS' && $tradeStatus != 'TRADE_FINISHED') { $this->error = 2622; return false; } $outTradeNo = isset($data['out_trade_no']) ? $data['out_trade_no'] : ''; if (empty($outTradeNo)) { $this->error = 2623; return false; } $payTotal = isset($data['total_amount']) ? floatval($data['total_amount']) : 0; $transactionId = isset($data['trade_no']) ? trim($data['trade_no']) : ''; $payAt = isset($data['send_pay_date']) ? trim($data['send_pay_date']) : date('Y-m-d H:i:s'); $notifyData = $data; if ($payTotal <= 0) { $this->error = 2624; return false; } } // 支付信息 $paymentInfo = $this->model->with(['user'])->where(['out_trade_no' => $outTradeNo, 'mark' => 1]) ->select(['user_id', 'order_no', 'pay_type', 'total_fee', 'status']) ->first(); $status = isset($paymentInfo['status']) ? $paymentInfo['status'] : 0; $totalFee = isset($paymentInfo['total_fee']) ? $paymentInfo['total_fee'] : 0; $paymentPayType = isset($paymentInfo['pay_type']) ? $paymentInfo['pay_type'] : 0; $orderNo = isset($paymentInfo['order_no']) ? $paymentInfo['order_no'] : ''; $payUserId = isset($paymentInfo['user_id']) ? $paymentInfo['user_id'] : 0; if (empty($paymentInfo) || empty($orderNo) || $payUserId <= 0) { $this->error = 2625; return false; } // 验证支付状态 if ($status == 1) { $this->error = 2626; return false; } // 验证支付方式 if ($paymentPayType != $payType) { $this->error = 2627; return false; } if ($payTotal != $totalFee || $payTotal <= 0) { $this->error = 2628; return false; } // 删除久远旧记录 $this->model->where(['mark' => 1])->where('create_time', '<=', time() - 60 * 86400)->delete(); // 更新订单数据 DB::beginTransaction(); $updateData = ['transaction_id' => $transactionId, 'result' => json_encode($notifyData, 256), 'pay_at' => $payAt, 'status' => 1, 'update_time' => time()]; if (!$this->model->where(['out_trade_no' => $outTradeNo, 'mark' => 1])->update($updateData)) { $this->error = 2632; DB::rollBack(); return false; } /* TODO 订单验证和状态处理 */ $orderInfo = []; // 商城订单支付 if ($scene == 'store') { $orderInfo = OrderModel::with(['user','orderGoods'])->where(['order_no' => $orderNo, 'mark' => 1]) ->select(['id as order_id', 'user_id', 'order_no', 'total as pay_money', 'pay_at as pay_time', 'remark', 'status']) ->first(); $orderStatus = isset($orderInfo['status']) ? $orderInfo['status'] : 0; // 验证订单 if (empty($orderInfo)) { DB::rollBack(); $this->error = 2629; return false; } // 订单状态 if ($orderStatus != 1) { DB::rollBack(); $this->error = 2630; return false; } $updateData = ['pay_at' => $payAt, 'transaction_id' => $transactionId, 'status' => 2, 'update_time' => time()]; if (!OrderModel::where(['order_no' => $orderNo, 'mark' => 1])->update($updateData)) { $this->error = 2633; DB::rollBack(); return false; } } // 退款 else if ($scene == 'refund') { $orderInfo = OrderModel::where(['order_no' => $orderNo, 'mark' => 1]) ->select(['id as order_id', 'user_id', 'order_no', 'total as pay_money', 'remark', 'refund_remark', 'pay_at as pay_time', 'refund_status as status']) ->first(); $refundRemark = isset($orderInfo['refund_remark']) ? $orderInfo['refund_remark'] : 'refund_remark'; $orderStatus = isset($orderInfo['status']) ? $orderInfo['status'] : 0; // 验证订单 if (empty($orderInfo)) { DB::rollBack(); $this->error = 2629; return false; } // 订单状态 if ($orderStatus != 2) { DB::rollBack(); $this->error = 2639; return false; } // 订单打款状态 if ($orderStatus == 1) { DB::rollBack(); $this->error = 2630; return false; } $updateData = ['refund_status' => 1, 'refund_remark' => $refundRemark ? $refundRemark . ' 已退款成功' : '已退款成功', 'update_time' => time()]; if (!OrderModel::where(['order_no' => $orderNo, 'mark' => 1])->update($updateData)) { $this->error = 2633; DB::rollBack(); return false; } } // TODO 场景业务回调处理 $orderUserId = isset($orderInfo['user_id']) ? $orderInfo['user_id'] : 0; $this->saveLog("caches:payments:notify_{$scene}:catch_{$orderNo}_{$orderUserId}", ['order' => $orderInfo, 'notify' => $data]); switch ($scene) { case 'store': // $userInfo = isset($orderInfo['user'])?$orderInfo['user']:[]; $orderTotal = isset($orderInfo['pay_money'])?$orderInfo['pay_money']:0; $orderGoods = isset($orderInfo['orderGoods'])?$orderInfo['orderGoods']:[]; $balance = isset($userInfo['balance'])? $userInfo['balance'] : 0; $goodsName = isset($orderGoods[0]['goods_name'])?$orderGoods[0]['goods_name']:''; $data = [ 'user_id'=>$orderUserId, 'source_order_no'=>$orderNo, 'type'=> 1, 'money'=> -$payTotal, 'after_money'=>$balance, 'date'=>date('Y-m-d'), 'create_time'=>time(), 'remark'=> '商品-'.$goodsName, 'status'=>1, 'mark'=>1 ]; if(!AccountLogModel::insertGetId($data)){ Db::rollBack(); $this->error = '付款处理失败'; } // 统计总消费 $id = AccountStatisticsModel::where(['user_id'=>$orderUserId])->orderBy('id','desc')->value('id'); $updateData = [ 'expend'=>DB::raw("expend + {$payTotal}"), 'updated_at'=>date('Y-m-d H:i:s') ]; if($id && !AccountStatisticsModel::where(['id'=>$id])->update($updateData)){ Db::rollBack(); $this->error = '统计处理失败'; }else{ $updateData['expend'] = $payTotal; $updateData['user_id'] = $orderUserId; $updateData['created_at'] = date('Y-m-d H:i:s'); if(!AccountStatisticsModel::insertGetId($updateData)){ Db::rollBack(); $this->error = '统计处理失败'; } } break; default: break; } $this->error = '回调处理成功'; DB::commit(); return true; } catch (\Exception $exception) { $this->error = $exception->getMessage(); RedisService::set("caches:payments:notify_{$scene}:catch_" . $orderNo . '_error', ['notify' => $data, 'error' => $exception->getMessage(), 'trace' => $exception->getTrace()], 7200); return false; } } /** * 日志 * @param $key * @param $data */ public function saveLog($key, $data) { if(env('APP_DEBUG')){ RedisService::set($key,$data,7200); } } /** * 退款请求 * @param $order * @param string $scene * @return bool * @throws \Yansongda\Pay\Exception\ContainerException * @throws \Yansongda\Pay\Exception\InvalidParamsException * @throws \Yansongda\Pay\Exception\ServiceNotFoundException */ public function refund($order, $scene = 'store') { $money = isset($order['money']) ? $order['money'] : 0; $payType = isset($order['pay_type']) && $order['pay_type']? $order['pay_type'] : 10; $orderNo = isset($order['order_no']) ? $order['order_no'] : ''; $outTradeNo = isset($order['out_trade_no']) ? $order['out_trade_no'] : ''; $transactionId = isset($order['transaction_id']) ? $order['transaction_id'] : ''; $remark = isset($order['remark']) && $order['remark'] ? $order['remark'] : '退款'; $pay = PaymentService::make()->createPay($scene, $payType); if (empty($pay)) { DB::rollBack(); $this->error = '创建退款支付失败'; return false; } // 保证金退款处理 $refundStatus = false; switch ($payType) { case 10: // 微信支付 $data = [ 'out_trade_no' => $outTradeNo?$outTradeNo:$orderNo, 'out_refund_no' => get_order_num('RF'), 'transaction_id' => $transactionId, 'notify_url' => url("/api/notify/{$scene}/{$payType}"), 'reason' => $remark, 'amount' => [ 'refund' => intval($money * 100), 'total' => intval($money * 100), 'currency' => 'CNY', ], ]; // 请求退款 $pay = $pay->refund($data); RedisService::set("caches:refunds:order:{$orderNo}_wxpay", ['data' => $data, 'pay' => $pay, 'type' => $payType, 'date' => date('Y-m-d H:i:s')], 7200); if ($pay->status == 'SUCCESS' || $pay->status == 'PROCESSING') { $refundStatus = true; } else { DB::rollBack(); $this->errorData = $data; $this->error = '微信退款处理失败:'.$pay->message; return false; } break; case 20: // 支付宝 $data = [ 'out_request_no' => $outTradeNo?$outTradeNo:$orderNo, 'trade_no' => $transactionId, 'refund_amount' => $money, 'query_options' => ['deposit_back_info'], 'refund_reason' => $remark, ]; $payResult = $pay->refund($data); RedisService::set("caches:refunds:order:{$orderNo}_alipay", ['data' => $data, 'pay' => $payResult, 'type' => $payType, 'date' => date('Y-m-d H:i:s')], 7200); if ($payResult->code == 10000 || intval($payResult->code) == 40004) { $refundStatus = true; } else { $this->errorData = $data; $this->error = '支付宝退款处理失败:'.$payResult->code; return false; } break; default: $this->error = '退款支付类型错误'; return false; } $this->error = '退款处理成功'; return $refundStatus; } /** * 分账 * @param $openid * @param $order * @param string $scene * @return array|\Psr\Http\Message\MessageInterface|\Yansongda\Supports\Collection|null * @throws \Yansongda\Pay\Exception\ContainerException * @throws \Yansongda\Pay\Exception\InvalidParamsException */ public function profitsharing($openid, $order ,$scene='sharing') { $data = [ 'type' => 'PERSONAL_OPENID', 'account' => $openid, 'relation_type' => 'SERVICE_PROVIDER' ]; $pay = PaymentService::make()->createPay($scene, 10); $plugin = $pay->mergeCommonPlugins([ AddReceiverPlugin::class ], $data); // 添加分账插件配置 $pay->pay($plugin, $data); // 分账数据 $transactionId = isset($order['transaction_id'])? $order['transaction_id'] : ''; // 支付交易单号 $profitsharingOrderNo = isset($order['out_order_no'])? $order['out_order_no'] : ''; // 分账单号,非支付单号 $body = isset($order['body'])? $order['body'] : ''; // 分账备注描述 $amount = isset($order['amount'])? $order['amount'] : ''; // 分账金额 $unsplit = isset($order['unsplit'])? $order['unsplit'] : true; // 是否限制只分账一次 $postData = [ 'transaction_id' => $transactionId, 'out_order_no' => $profitsharingOrderNo, 'receivers' => [ [ 'type' => 'PERSONAL_OPENID', 'account' => $openid, 'amount' => intval($amount*100), // 分账金额,单位:分 'description' => $body, //分账描述 ], ], 'unfreeze_unsplit' => $unsplit, ]; // 创建分账数据 $createPlugin = $pay->mergeCommonPlugins([ CreatePlugin::class ], $postData); // 发起分账 $result = $pay->pay($createPlugin, $postData); return $result; } }