// +---------------------------------------------------------------------- namespace App\Services; use App\Models\AccountLogModel; use App\Models\BalanceLogModel; use App\Models\DepositModel; use App\Models\MemberModel; use App\Models\MessageModel; use App\Models\OrderModel; use App\Models\PaymentModel; use App\Models\VideoOrderModel; use App\Models\VipModel; use App\Services\Api\MessageService; use Illuminate\Support\Facades\DB; use Yansongda\Pay\Pay; 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 场景,vip-VIP会员 * @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); 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'){ // 公众号 $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; 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; } /** * 微信小程序支付 * @param $userInfo * @param $order * @param string $scene * @return false|\Yansongda\Supports\Collection */ public function minPay($userInfo, $order, $scene = 'pay') { $amount = isset($order['pay_money']) ? $order['pay_money'] : 0; $openid = isset($order['openid']) ? $order['openid'] : ''; 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('is') . 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, ], ]; // 创建支付 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 = 'pay') { $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('is') . 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 * 100), '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 = 'deposit') { $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('is') . 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 场景 deposit-保证金,depositRefund-保证金退款,withdraw-收入提现 * @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; $payUser = isset($paymentInfo['user']) ? $paymentInfo['user'] : []; 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 = []; // VIP购买 if ($scene == 'vip') { $orderInfo = OrderModel::with(['vip'])->where(['order_no' => $orderNo, 'mark' => 1]) ->select(['id as order_id', 'user_id', 'goods_id', 'expired_at', '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; } } // 视频单集购买 if ($scene == 'course') { $orderInfo = VideoOrderModel::where(['order_no' => $orderNo, 'mark' => 1]) ->select(['id as order_id', 'user_id', 'goods_id', 'expired_at', '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 (!VideoOrderModel::where(['order_no' => $orderNo, 'mark' => 1])->update($updateData)) { $this->error = 2633; DB::rollBack(); return false; } } // VIP退款 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', 'pay_at as pay_time', 'refund_status as status']) ->first(); $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 == 4) { DB::rollBack(); $this->error = 2630; return false; } $updateData = ['refund_status' => 4, '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; RedisService::set("caches:payments:notify_{$scene}:catch_{$orderNo}_{$orderUserId}", ['order' => $orderInfo, 'notify' => $data], 7200); switch ($scene) { case 'vip': // 购买VIP $vipInfo = isset($orderInfo['vip']) ? $orderInfo['vip'] : []; $vipType = isset($vipInfo['type']) ? $vipInfo['type'] : 0; $expiredAt = isset($orderInfo['expired_at']) ? $orderInfo['expired_at'] : ''; $price = isset($orderInfo['pay_money']) ? $orderInfo['pay_money'] : 0; $remark = isset($orderInfo['remark']) && $orderInfo['remark'] ? $orderInfo['remark'] : '购买VIP会员'; if (empty($vipInfo) || $expiredAt <= date('Y-m-d H:i:s') || $vipType <= 0) { DB::rollBack(); $this->error = 'VIP参数错误'; return false; } $field = "zg_vip"; if ($vipType == 2) { $field = "zsb_vip"; } else if ($vipType == 3) { $field = "video_vip"; } $updateData = ["is_{$field}" => 1, "{$field}_expired" => $expiredAt, 'update_time' => time()]; RedisService::set("caches:payments:notify_{$scene}:catch_{$orderNo}_{$orderUserId}_update", ['order' => $orderInfo, 'update' => $updateData, 'notify' => $data], 600); if (!MemberModel::where(['id' => $orderUserId, 'mark' => 1])->update($updateData)) { DB::rollBack(); $this->error = 2639; return false; } // 账单记录 $log = [ 'user_id' => $orderUserId, 'source_order_no' => $orderNo, 'type' => 1, 'money' => $price, 'date' => date('Y-m-d H:i:s'), 'create_time' => time(), 'remark' => $remark, 'status' => 1, 'mark' => 1 ]; RedisService::set("caches:payments:notify_{$scene}:catch_{$orderNo}_{$orderUserId}_log", ['order' => $orderInfo, 'log' => $log, 'notify' => $data], 600); if (!AccountLogModel::insertGetId($log)) { $this->error = 2635; return false; } break; case 'course': $price = isset($orderInfo['pay_money']) ? $orderInfo['pay_money'] : 0; $remark = isset($orderInfo['remark']) && $orderInfo['remark'] ? $orderInfo['remark'] : '购买VIP会员'; // 账单记录 $log = [ 'user_id' => $orderUserId, 'source_order_no' => $orderNo, 'type' => 2, 'money' => $price, 'date' => date('Y-m-d H:i:s'), 'create_time' => time(), 'remark' => $remark, 'status' => 1, 'mark' => 1 ]; RedisService::set("caches:payments:notify_{$scene}:catch_{$orderNo}_{$orderUserId}_log", ['order' => $orderInfo, 'log' => $log, 'notify' => $data], 600); if (!AccountLogModel::insertGetId($log)) { $this->error = 2635; return false; } break; default: break; } $this->error = 2638; 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 $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='vip') { $money = isset($order['money'])? $order['money'] : 0; $payType = isset($order['pay_type'])? $order['pay_type'] : 0; $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 = 2171; return false; } // 保证金退款处理 $refundStatus = false; switch ($payType) { case 10: // 微信支付 $data = [ 'out_trade_no' => $outTradeNo, '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->error = 2172; return false; } break; case 20: // 支付宝 $data = [ 'out_request_no' => $outTradeNo, '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->error = 2173; return false; } break; default: $this->error = 2179; return false; } $this->error = 2176; return $refundStatus; } }