6
0
Fork 0

微信支付

release
李静 2021-12-27 13:33:55 +08:00
parent 959f8d3503
commit f0c94a6356
12 changed files with 335 additions and 111 deletions

View File

@ -6,47 +6,49 @@ use App\Events\OrderPaid;
use App\Exceptions\BizException;
use App\Models\Order;
use App\Models\OrderRefundLog;
use App\Services\OrderService;
use App\Services\PayService;
use App\Services\WeChatPayService;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Throwable;
class WeChatPayController extends Controller
{
/**
* 订单支付结果通知
* 支付结果通知
*
* @param \App\Services\WeChatPayService $weChatPayService
* @return \Illuminate\Http\Response
*/
public function orderPaidNotify(WeChatPayService $weChatPayService)
public function paidNotify(WeChatPayService $weChatPayService)
{
return $weChatPayService->handlePaidNotify(function ($message, $fail) {
$this->log('order paid notify', $message);
// 通信失败
if (data_get($message, 'return_code') !== 'SUCCESS') {
return $fail('通信失败');
}
// 支付失败
if (data_get($message, 'result_code') !== 'SUCCESS') {
return true;
}
try {
$order = DB::transaction(function () use ($message) {
return (new OrderService())->paySuccess($message['out_trade_no'], [
'pay_sn' => $message['transaction_id'],
'pay_way' => Order::PAY_WAY_WXPAY,
$payLog = DB::transaction(function () use ($message) {
$payService = new PayService();
if (data_get($message, 'result_code') !== 'SUCCESS') {
return $payService->payFailed($message['out_trade_no'], [
'pay_sn' => $message['transaction_id'] ?? null,
'failed_reason' => '['.$message['err_code'].']'.$message['err_code_des'],
]);
}
return $payService->paySuccess($message['out_trade_no'], [
'out_trade_no' => $message['transaction_id'],
'pay_at' => Carbon::parse($message['time_end']),
]);
});
OrderPaid::dispatchIf($order->isPaid(), $order);
if ($payLog->payable instanceof Order) {
OrderPaid::dispatchIf($payLog->payable->isPaid(), $payLog->payable);
}
} catch (ModelNotFoundException | BizException $e) {
} catch (Throwable $e) {
throw $e;
@ -65,8 +67,6 @@ class WeChatPayController extends Controller
public function orderRefundedNotify(WeChatPayService $weChatPayService)
{
return $weChatPayService->handleRefundedNotify(function ($message, $reqInfo, $fail) {
$this->log('order refunded notify', $reqInfo);
// 通信失败
if (data_get($message, 'return_code') !== 'SUCCESS') {
return $fail('通信失败');
@ -85,19 +85,4 @@ class WeChatPayController extends Controller
return true;
});
}
/**
* 微信回调日志
*
* @param string $message
* @param array $context
* @return void
*/
protected function log(string $message, array $context = [])
{
return Log::build([
'driver' => 'daily',
'path' => storage_path('logs/wxpay.log'),
])->info($message, $context);
}
}

View File

@ -7,5 +7,5 @@ use Illuminate\Support\Facades\Route;
//快递100物流推送
Route::post('kuaidi100', [Kuaidi100Controller::class, 'notify']);
// 微信支付通知
Route::post('wxpay/order-paid-notify', [WeChatPayController::class, 'orderPaidNotify'])->name('wxpay.order_paid_notify');
Route::post('wxpay/paid-notify', [WeChatPayController::class, 'paidNotify'])->name('wxpay.paid_notify');
Route::post('wxpay/order-refund-notify', [WeChatPayController::class, 'orderRefundedNotify'])->name('wxpay.order_refund_notify');

View File

@ -1,16 +0,0 @@
<?php
namespace App\Helpers;
class Order
{
/**
* 生成订单流水号
*
* @return string
*/
public static function serialNumber(): string
{
return date('YmdHis').sprintf('%06d', mt_rand(1, 999999));
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Listeners;
use App\Events\OrderPaid;
use App\Services\CouponService;
use Illuminate\Support\Facades\DB;
use Throwable;
class SendCoupons
{
/**
* @param \App\Services\CouponService $couponService
*/
public function __construct(
public CouponService $couponService,
) {
}
/**
* Handle the event.
*
* @param \App\Events\OrderPaid $event
* @return void
*/
public function handle(OrderPaid $event)
{
$order = $event->order;
if (is_null($order)) {
return;
}
// 处理购买分区商品送券
try {
DB::beginTransaction();
$products = $order->products()->with('sku.parts')->get();
// 整理订单商品的分区
$inValidParts = [];
foreach ($products->pluck('sku.parts') as $parts) {
foreach ($parts as $part) {
if ($part->is_show) {
// 分区去重
$inValidParts[$part->id] = $part;
}
}
}
foreach ($inValidParts as $inValidPart) {
$this->couponService->receivePartCoupon($inValidPart, $order->user);
}
DB::commit();
} catch (Throwable $th) {
DB::rollBack();
report($th);
}
}
}

View File

@ -36,7 +36,6 @@ class Order extends Model
public const PAY_WAY_WALLET = 'wallet'; // 钱包
public const PAY_WAY_BALANCE = 'balance'; // 余额
public const PAY_WAY_OFFLINE = 'offline'; // 现金支付
public const PAY_WAY_NONE = 'none'; // 无
/**
* @var array
@ -74,6 +73,7 @@ class Order extends Model
'pay_sn',
'pay_way',
'pay_at',
'out_trade_no',
'consignee_name',
'consignee_telephone',
'consignee_zone',
@ -154,6 +154,14 @@ class Order extends Model
return $this->belongsToMany(Tag::class, 'taggables', 'taggable_id', 'tag_id')->wherePivot('taggable_type', self::class)->withTimestamps();
}
/**
* 属于此订单的支付记录
*/
public function payLogs()
{
return $this->morphMany(PayLog::class, 'payable');
}
/**
* 此订单是否待付款
*

View File

@ -0,0 +1,63 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PayLog extends Model
{
/**
* 支付状态
*/
public const STATUS_PENDING = 0; // 待付款
public const STATUS_SUCCESS = 1; // 成功
public const STATUS_FAILED = 2; // 失败
/**
* 支付方式
*/
public const PAY_WAY_WXPAY = 'wxpay'; // 微信支付
public const PAY_WAY_ALIPAY = 'alipay'; // 支付宝
/**
* @var array
*/
protected $attributes = [
'status' => self::STATUS_PENDING,
];
/**
* @var array
*/
protected $casts = [
'pay_at' => 'datetime',
];
/**
* @var array
*/
protected $fillable = [
'pay_sn',
'pay_way',
'pay_at',
'out_trade_no',
'status',
'failed_reason',
];
/**
* 获取支付记录所属的模型
*/
public function payable()
{
return $this->morphTo();
}
/**
* 获取支付记录是否是待付款
*/
public function isPending()
{
return $this->status === static::STATUS_PENDING;
}
}

View File

@ -18,6 +18,7 @@ class EventServiceProvider extends ServiceProvider
],
\App\Events\OrderPaid::class => [
\App\Listeners\OrderPaidNotify::class,
\App\Listeners\SendCoupons::class,
],
];

View File

@ -9,7 +9,6 @@ use App\Endpoint\Api\Http\Resources\UserCouponResource;
use App\Exceptions\BizException;
use App\Exceptions\ShippingNotSupportedException;
use App\Helpers\Numeric;
use App\Helpers\Order as OrderHelper;
use App\Models\DistributionPreIncomeJob;
use App\Models\Order;
use App\Models\OrderProduct;
@ -20,7 +19,6 @@ use App\Models\User;
use App\Models\UserCoupon;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Throwable;
class OrderService
{
@ -144,7 +142,7 @@ class OrderService
$totalAmount += $shippingFee;
$orderAttrs = [
'sn' => OrderHelper::serialNumber(),
'sn' => serial_number(),
'user_coupon_id' => $coupon?->id,
'coupon_discount_amount' => $couponDiscountAmount,
'vip_discount_amount' => $vipDiscountAmount,
@ -162,8 +160,8 @@ class OrderService
if ($totalAmount === 0) {
$orderAttrs = array_merge([
'status' => Order::STATUS_PAID,
'pay_sn' => OrderHelper::serialNumber(),
'pay_way' => Order::PAY_WAY_NONE,
'pay_sn' => serial_number(),
'pay_way' => Order::PAY_WAY_BALANCE,
'pay_at' => now(),
]);
}
@ -708,20 +706,24 @@ class OrderService
throw new BizException('订单状态不是待付款');
}
if (in_array($payWay, $this->wxpayWays)) {
return (new WeChatPayService())->pay([
'attach' => json_encode([
'pay_way' => $payWay,
]),
'body' => app_settings('app.app_name').'-商城订单',
'out_trade_no' => $order->sn,
'total_fee' => $order->total_amount,
'notify_url' => url(route('wxpay.order_paid_notify', [], false), [], true),
'trade_type' => PayWay::$wxpayTradeTypes[$payWay],
]);
}
$payLog = $order->payLogs()->create([
'pay_sn' => serial_number(),
'pay_way' => $payWay,
]);
throw new BizException('支付方式不支持');
switch ($payWay) {
case PayWay::WXPAY_APP:
case PayWay::WXPAY_H5:
case PayWay::WXPAY_JSAPI:
case PayWay::WXPAY_MINI:
return (new WeChatPayService())->pay([
'body' => app_settings('app_name').'-商城订单',
'out_trade_no' => $payLog->pay_sn,
'total_fee' => $order->total_amount,
'trade_type' => PayWay::$wxpayTradeTypes[$payWay],
]);
break;
}
}
/**
@ -731,10 +733,8 @@ class OrderService
* @param array $params
* @return \App\Models\Order
*/
public function paySuccess(string $sn, array $params = []): Order
public function paySuccess(Order $order, array $params = []): Order
{
$order = Order::where('sn', $sn)->firstOrFail();
if (! $order->isPending()) {
throw new BizException('订单状态不是待支付');
}
@ -743,19 +743,10 @@ class OrderService
'pay_sn' => $params['pay_sn'],
'pay_way' => $params['pay_way'],
'pay_at' => $params['pay_at'],
'out_trade_no' => $params['out_trade_no'],
'status' => Order::STATUS_PAID,
]);
// 处理购买分区商品送券
try {
DB::beginTransaction();
$this->sendPartCoupon($order);
DB::commit();
} catch (Throwable $th) {
DB::rollBack();
report($th);
}
DistributionPreIncomeJob::create([
'jobable_id' => $order->id,
'jobable_type' => $order->getMorphClass(),
@ -803,7 +794,7 @@ class OrderService
if ($order->isWaitShipping()) {
$refundLog = $order->refundLogs()->create([
'sn' => OrderHelper::serialNumber(),
'sn' => serial_number(),
'amount' => $order->total_amount,
'reason' => '取消订单',
]);
@ -857,31 +848,4 @@ class OrderService
'status' => Order::STATUS_CANCELLED,
]);
}
/**
* 发送分区优惠券
*
* @param Order $order
* @return void
*/
protected function sendPartCoupon(Order $order)
{
$products = $order->products()->with('sku.parts')->get();
// 整理订单商品的分区
$inValidParts = [];
foreach ($products->pluck('sku.parts') as $parts) {
foreach ($parts as $part) {
if ($part->is_show) {
// 分区去重
$inValidParts[$part->id] = $part;
}
}
}
// 将有效的分区丢进去领券
$couponService = new CouponService();
foreach ($inValidParts as $inValidPart) {
$couponService->receivePartCoupon($inValidPart, $order->user);
}
}
}

View File

@ -0,0 +1,86 @@
<?php
namespace App\Services;
use App\Constants\PayWay;
use App\Exceptions\BizException;
use App\Models\Order;
use App\Models\PayLog;
class PayService
{
/**
* 支付成功
*
* @param string $sn
* @param array $params
* @return \App\Models\PayLog
*
* @throws \App\Exceptions\BizException
*/
public function paySuccess(string $sn, array $params = []): PayLog
{
$payLog = PayLog::where('pay_sn', $sn)->firstOrFail();
if (! $payLog->isPending()) {
throw new BizException('支付记录状态异常');
}
$payLog->update([
'pay_at' => $params['pay_at'] ?? now(),
'out_trade_no' => $params['out_trade_no'] ?? null,
'status' => PayLog::STATUS_SUCCESS,
]);
if ($payLog->payable instanceof Order) {
switch ($payLog->pay_way) {
case PayWay::WXPAY_APP:
case PayWay::WXPAY_H5:
case PayWay::WXPAY_JSAPI:
case PayWay::WXPAY_MINI:
case PayWay::WXPAY_NATIVE:
$payWay = Order::PAY_WAY_WXPAY;
break;
default:
$payWay = $payLog->pay_way;
break;
}
(new OrderService())->paySuccess($payLog->payable, [
'pay_sn' => $payLog->pay_sn,
'pay_way' => $payWay,
'pay_at' => $payLog->pay_at,
'out_trade_no' => $payLog->out_trade_no,
]);
}
return $payLog;
}
/**
* 支付失败
*
* @param string $sn
* @param array $params
* @return \App\Models\PayLog
*
* @throws \App\Exceptions\BizException
*/
public function payFailed(string $sn, array $params = []): PayLog
{
$payLog = PayLog::where('pay_sn', $sn)->firstOrFail();
if (! $payLog->isPending()) {
throw new BizException('支付记录状态异常');
}
$payLog->update([
'out_trade_no' => $params['out_trade_no'] ?? null,
'status' => PayLog::STATUS_FAILED,
'failed_reason' => $params['failed_reason'] ?? null,
]);
return $payLog;
}
}

View File

@ -9,7 +9,7 @@ return [
'key' => env('WECHAT_PAYMENT_KEY'),
'cert_path' => env('WECHAT_PAYMENT_CERT_PATH'), // 绝对地址
'key_path' => env('WECHAT_PAYMENT_KEY_PATH'), // 绝对地址
'notify_url' => env('WECHAT_PAYMENT_NOTIFY_URL'),
'notify_url' => url(route('wxpay.paid_notify', [], false), [], true),
],
],
];

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePayLogsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('pay_logs', function (Blueprint $table) {
$table->id();
$table->morphs('payable');
$table->string('pay_sn')->unique()->comment('支付流水号');
$table->string('pay_way')->comment('支付方式');
$table->string('out_trade_no')->nullable()->comment('外部交易单号');
$table->timestamp('pay_at')->nullable()->comment('支付时间');
$table->tinyInteger('status')->comment('状态: 0 待付款, 1 支付成功, 2 支付失败');
$table->string('failed_reason')->nullable()->comment('失败原因');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('pay_logs');
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddTradeOutNoToOrdersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('orders', function (Blueprint $table) {
$table->string('out_trade_no')->nullable()->comment('外部交易单号');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn(['out_trade_no']);
});
}
}