diff --git a/laravel/laravel5/10.md b/laravel/laravel5/10.md
new file mode 100644
index 0000000..2682710
--- /dev/null
+++ b/laravel/laravel5/10.md
@@ -0,0 +1,450 @@
+# 退款-申请退款
+1. 创建申请退款的请求类 `php artisan make:request ApplyRefundRequest`
+```
+ 'required',
+ ];
+ }
+
+ public function attributes()
+ {
+ return [
+ 'reason' => '原因',
+ ];
+ }
+}
+```
+2. 增加申请退款的方法 app/Http/Controllers/OrdersController@applyRefund
+```
+use App\Http\Requests\ApplyRefundRequest;
+
+...
+
+ /**
+ * 申请退款
+ */
+ public function applyRefund(Order $order, ApplyRefundRequest $request)
+ {
+ // 校验订单是否属于当前用户
+ $this->authorize('own', $order);
+ // 判断订单是否已付款
+ if (!$order->paid_at) {
+ throw new InvalidRequestException('该订单未支付,不可退款');
+ }
+ // 判断订单退款状态是否正确
+ if ($order->refund_status !== Order::REFUND_STATUS_PENDING) {
+ throw new InvalidRequestException('该订单已经申请过退款,请勿重复申请');
+ }
+ // 将用户输入的退款理由放到订单的 extra 字段中
+ $extra = $order->extra ?: [];
+ $extra['refund_reason'] = $request->input('reason');
+ // 将订单退款状态改为已申请退款
+ $order->update([
+ 'refund_status' => Order::REFUND_STATUS_APPLIED,
+ 'extra' => $extra,
+ ]);
+
+ return $order;
+ }
+```
+3. 增加路由 `Route::post('orders/{order}/apply_refund', 'OrdersController@applyRefund')->name('orders.apply_refund'); //申请退款`
+4. 增加入口链接 ../orders/show.blade.php(同时在视图层作一次判断:如果成功申请退款则展示退款状态)
+```
+# 展示退款状态
+{{-- 物流信息 --}}
+@if($order->ship_data)
+ ...
+@endif
+{{-- 退款信息 --}}
+@if($order->paid_at && $order->refund_status !== \App\Models\Order::REFUND_STATUS_PENDING)
+
+ @if($order->couponCode)
+
+
优惠信息:
+
{{ $order->couponCode->description }}
+
+ @endif
+
+
订单总价:
+
¥{{ $order->total_amount }}
+
+```
+3. 当订单金额不满足优惠券使用最低价格的时候展示错误信息 ../cart/index.blade.php
+```
+ // 生成订单
+ $('.btn-create-order').click(function () {
+ // 构建请求参数,将用户选择的地址的 id 和备注内容写入请求参数
+ var req = {
+ address_id: $('#order-form').find('select[name=address]').val(),
+ items: [],
+ remark: $('#order-form').find('textarea[name=remark]').val(),
+ coupon_code: $('input[name=coupon_code]').val(), // <=从优惠码输入框中获取优惠码
+ };
+
+ ...
+
+ axios.post('{{ route('orders.store') }}', req)
+ .then(function (response) {
+ swal('订单提交成功', '', 'success')
+ .then(() => {
+ location.href = '/orders/' + response.data.id;
+ });
+ }, function (error) {
+
+ ...
+
+ // **添加这里**
+ } else if (error.response.status === 403) { // 这里判断状态 403
+ swal(error.response.data.msg, '', 'error');
+ } else {
+ swal('系统错误', '', 'error');
+ }
+ });
+ });
+```
+* 关闭订单的时候,如果订单使用了优惠券,那么把优惠券的使用次数减回去。
+```
+ public function handle()
+ {
+ // 判断对应的订单是否已经被支付
+ // 如果已经支付则不需要关闭订单,直接退出
+ if ($this->order->paid_at) {
+ return;
+ }
+ // 通过事务执行 sql
+ \DB::transaction(function() {
+
+ ...
+
+ // **减少优惠券已用次数**
+ if ($this->order->couponCode) {
+ $this->order->couponCode->changeUsed(false);
+ }
+ });
+ }
+```
\ No newline at end of file
diff --git a/laravel/laravel5/12.md b/laravel/laravel5/12.md
new file mode 100644
index 0000000..c741ec9
--- /dev/null
+++ b/laravel/laravel5/12.md
@@ -0,0 +1,211 @@
+> 11 节结束后,整个项目的功能就已经全部完成了,接下来处理一些小逻辑
+
+# 后台权限分配
+1. 以 admin 的身份登陆后台管理,创建权限:商品管理、订单管理、优惠券管理
+2. 将权限交给之前创建的运营角色即可
+
+# 虚拟数据填充
+1. 用户数据
+ * UserFactory
+ ```
+ return [
+ 'name' => $faker->name,
+ 'email' => $faker->unique()->safeEmail,
+ 'password' => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
+ 'remember_token' => str_random(10),
+ 'email_verified' => true, // <= 因为数据表中增加了字段,这里也虚拟生成
+ ];
+ ```
+ * 创建 Seeder `php artisan make:seed UsersTableSeeder`
+ ```
+ // run()
+ // 生成 100 条假数据
+ factory(\App\Models\User::class, 100)->create();
+ ```
+2. 收货地址
+ * 模型工厂之前开发时写过了。
+ * 创建 Seeder `php artisan make:seed UserAddressesTableSeeder`
+ ```
+ use App\Models\User;
+ use App\Models\UserAddress;
+
+ ...
+
+ public function run()
+ {
+ User::all()->each(function (User $user) {
+ factory(UserAddress::class, random_int(1, 3))->create(['user_id' => $user->id]);
+ });
+ }
+ ```
+3. 优惠券
+ * 模型工厂写过了
+ * `php artisan make:seed CouponCodesTableSeeder`
+ ```
+ // run()
+ factory(\App\Models\CouponCode::class, 20)->create();
+ ```
+4. 订单
+ * 创建模型工厂 `php artisan make:factory OrderFactory --model=Models/Order` => 大订单
+ ```
+ define(Order::class, function (Faker $faker) {
+ // 随机取一个用户
+ $user = User::query()->inRandomOrder()->first();
+ // 随机取一个该用户的地址
+ $address = $user->addresses()->inRandomOrder()->first();
+ // 10% 的概率把订单标记为退款
+ $refund = random_int(0, 10) < 1;
+ // 随机生成发货状态
+ $ship = $faker->randomElement(array_keys(Order::$shipStatusMap));
+ // 优惠券
+ $coupon = null;
+ // 30% 概率该订单使用了优惠券
+ if (random_int(0, 10) < 3) {
+ // 为了避免出现逻辑错误,我们只选择没有最低金额限制的优惠券
+ $coupon = CouponCode::query()->where('min_amount', 0)->inRandomOrder()->first();
+ // 增加优惠券的使用量
+ $coupon->changeUsed();
+ }
+
+ return [
+ 'address' => [
+ 'address' => $address->full_address,
+ 'zip' => $address->zip,
+ 'contact_name' => $address->contact_name,
+ 'contact_phone' => $address->contact_phone,
+ ],
+ 'total_amount' => 0,
+ 'remark' => $faker->sentence,
+ 'paid_at' => $faker->dateTimeBetween('-30 days'), // 30天前到现在任意时间点
+ 'payment_method' => $faker->randomElement(['wechat', 'alipay']),
+ 'payment_no' => $faker->uuid,
+ 'refund_status' => $refund ? Order::REFUND_STATUS_SUCCESS : Order::REFUND_STATUS_PENDING,
+ 'refund_no' => $refund ? Order::getAvailableRefundNo() : null,
+ 'closed' => false,
+ 'reviewed' => random_int(0, 10) > 2,
+ 'ship_status' => $ship,
+ 'ship_data' => $ship === Order::SHIP_STATUS_PENDING ? null : [
+ 'express_company' => $faker->company,
+ 'express_no' => $faker->uuid,
+ ],
+ 'extra' => $refund ? ['refund_reason' => $faker->sentence] : [],
+ 'user_id' => $user->id,
+ 'coupon_code_id' => $coupon ? $coupon->id : null,
+ ];
+ });
+ ```
+ * 创建模型工厂 `php artisan make:factory OrderItemFactory --model=Models/OrderItem` => 订单详情
+ ```
+ define(OrderItem::class, function (Faker $faker) {
+ // 从数据库随机取一条商品
+ $product = Product::query()->where('on_sale', true)->inRandomOrder()->first();
+ // 从该商品的 SKU 中随机取一条
+ $sku = $product->skus()->inRandomOrder()->first();
+
+ return [
+ 'amount' => random_int(1, 5), // 购买数量随机 1 - 5 份
+ 'price' => $sku->price,
+ 'rating' => null,
+ 'review' => null,
+ 'reviewed_at' => null,
+ 'product_id' => $product->id,
+ 'product_sku_id' => $sku->id,
+ ];
+ });
+ ```
+ * 创建 Seeder `php artisan make:seeder OrdersSeeder`
+ ```
+ create();
+ // 被购买的商品,用于后面更新商品销量和评分
+ $products = collect([]);
+ foreach ($orders as $order) {
+ // 每笔订单随机 1 - 3 个商品
+ $items = factory(OrderItem::class, random_int(1, 3))->create([
+ 'order_id' => $order->id,
+ 'rating' => $order->reviewed ? random_int(1, 5) : null, // 随机评分 1 - 5
+ 'review' => $order->reviewed ? $faker->sentence : null,
+ 'reviewed_at' => $order->reviewed ? $faker->dateTimeBetween($order->paid_at) : null, // 评价时间不能早于支付时间
+ ]);
+
+ // 计算总价
+ $total = $items->sum(function (OrderItem $item) {
+ return $item->price * $item->amount;
+ });
+
+ // 如果有优惠券,则计算优惠后价格
+ if ($order->couponCode) {
+ $total = $order->couponCode->getAdjustedPrice($total);
+ }
+
+ // 更新订单总价
+ $order->update([
+ 'total_amount' => $total,
+ ]);
+
+ // 将这笔订单的商品合并到商品集合中
+ $products = $products->merge($items->pluck('product'));
+ }
+
+ // 根据商品 ID 过滤掉重复的商品
+ $products->unique('id')->each(function (Product $product) {
+ // 查出该商品的销量、评分、评价数
+ $result = OrderItem::query()
+ ->where('product_id', $product->id)
+ ->whereHas('order', function ($query) {
+ $query->whereNotNull('paid_at');
+ })
+ ->first([
+ \DB::raw('count(*) as review_count'),
+ \DB::raw('avg(rating) as rating'),
+ \DB::raw('sum(amount) as sold_count'),
+ ]);
+
+ $product->update([
+ 'rating' => $result->rating ?: 5, // 如果某个商品没有评分,则默认为 5 分
+ 'review_count' => $result->review_count,
+ 'sold_count' => $result->sold_count,
+ ]);
+ });
+ }
+ }
+ ```
+5. 刷新数据库
+ * 现在 DatabaseSeeder.php 中注册我们上面写的 Seeder
+ ```
+ public function run()
+ {
+ $this->call(UsersTableSeeder::class); //用户表
+ $this->call(UserAddressesTableSeeder::class); //收货地址表
+ $this->call(ProductsSeeder::class); //商品(sku)表
+ $this->call(OrdersSeerder::class); //订单(item)表
+ $this->call(CouponCodesTableSeeder::class); //优惠券
+ }
+ ```
+ * 执行迁移并刷新数据库 `php artisan migrate:refresh --seed`
\ No newline at end of file
diff --git a/laravel/laravel5/6.md b/laravel/laravel5/6.md
new file mode 100644
index 0000000..f3c7a79
--- /dev/null
+++ b/laravel/laravel5/6.md
@@ -0,0 +1,288 @@
+# 购物车功能
+> 购物车的数据通常会保存到 Session 或者数据库。对于拥有多个端(网页、App)的电商网站来说为了保障用户体验会使用数据库来保存购物车中的数据,这样用户在网页端加入购物车的商品也能在 App 中看到。
+
+# 创建模型和数据表
+* 命令 `php artisan make:model Models/CartItem -m`
+```
+$table->increments('id');
+$table->unsignedInteger('user_id');
+$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+$table->unsignedInteger('product_sku_id');
+$table->foreign('product_sku_id')->references('id')->on('product_skus')->onDelete('cascade');
+$table->unsignedInteger('amount'); //数量
+```
+* 编辑模型 CartItem
+```
+ /**
+ * 可填字段
+ */
+ protected $fillable = ['amount'];
+
+ /**
+ * 禁用时间戳
+ */
+ public $timestamps = false;
+
+ /**
+ * n:1 User
+ */
+ public function user()
+ {
+ return $this->belongsTo(User::class);
+ }
+
+ /**
+ * n:1 ProductSku
+ */
+ public function productSku()
+ {
+ return $this->belongsTo(ProductSku::class);
+ }
+```
+* 在 User 模型中绑定多对一关系
+```
+ /**
+ * 购物车
+ * 1:n CartItem
+ */
+ public function cartItems()
+ {
+ return $this->hasMany(CartItem::class);
+ }
+```
+> 最后执行迁移 `php artisan migrate`
+
+# 添加商品到购物车
+* 创建控制器 `php artisan make:controller CartController`
+* 创建请求类 `php artisan make:request AddCartRequest` => 添加商品的时候要校验一下\
+```
+ [
+ 'required',
+ function ($attribute, $value, $fail) {
+ if (!$sku = ProductSku::find($value)) {
+ $fail('该商品不存在');
+ return;
+ }
+ if (!$sku->product->on_sale) {
+ $fail('该商品未上架');
+ return;
+ }
+ if ($sku->stock === 0) {
+ $fail('该商品已售完');
+ return;
+ }
+ if ($this->input('amount') > 0 && $sku->stock < $this->input('amount')) {
+ $fail('该商品库存不足');
+ return;
+ }
+ },
+ ],
+ 'amount' => ['required', 'integer', 'min:1'],
+ ];
+ }
+
+ /**
+ * 字段名称
+ */
+ public function attributes()
+ {
+ return [
+ 'amount' => '商品数量'
+ ];
+ }
+
+ /**
+ * 提示信息
+ */
+ public function messages()
+ {
+ return [
+ 'sku_id.required' => '请选择商品'
+ ];
+ }
+}
+```
+> 注意继承之前创建的基础请求类 Request 而不是自动创建时溜出来的 FormRequest
+* 编辑控制器的添加方法 CartController@add
+```
+// 引用请求类和模型
+use app\Http\Request\AddCartRequest;
+use App\Models\CartItem;
+
+...
+
+ /**
+ * 添加商品到购物车
+ */
+ public function add(AddCartRequest $request)
+ {
+ // 获取请求用户、商品id、添加的数量
+ $user = $request->user();
+ $skuId = $request->input('sku_id');
+ $amount = $request->input('amount');
+
+ // 判断商品是否已经存在
+ if ($cart = $user->cartItems()->where('product_sku_id', $skuId)->first()) {
+ // 如果存在则直接叠加商品数量
+ $cart->update([
+ 'amount' => $cart->amount + $amount,
+ ]);
+ } else {
+ // 否则创建一个新的购物车记录
+ $cart = new CartItem(['amount' => $amount]);
+ $cart->user()->associate($user);
+ $cart->productSku()->associate($skuId);
+ $cart->save();
+ }
+
+ return [];
+ }
+```
+> `$cart->user()->associate($user)` => 关联 $cart 和 $user (也就相当于做了一次赋值 `$cart->user_id = $user->id`)
+* 给这个方法配置路由
+```
+// 以下是已验证邮箱的用户可以访问的路由
+Route::group(['middleware' => 'email_verified'], function() {
+ ...
+ Route::post('cart', 'CartController@add')->name('cart.add'); //添加商品到购物车
+});
+```
+* 处理视图 ../products/show.blade.php
+```
+ // 添加商品到购物车
+ $('.btn-add-to-cart').click(function () {
+ // 请求加入购物车接口
+ axios.post('{{ route('cart.add') }}', {
+ sku_id: $('label.active input[name=skus]').val(),
+ amount: $('.cart_amount input').val(),
+ })
+ .then(function () { // 请求成功执行此回调
+ swal('加入购物车成功', '', 'success');
+ }, function (error) { // 请求失败执行此回调
+ if (error.response.status === 401) {
+
+ // http 状态码为 401 代表用户未登陆
+ swal('请先登录', '', 'error');
+
+ } else if (error.response.status === 422) {
+
+ // http 状态码为 422 代表用户输入校验失败
+ var html = '
';
+ _.each(error.response.data.errors, function (errors) {
+ _.each(errors, function (error) {
+ html += error + '
';
+ })
+ });
+ html += '
';
+ swal({ content: $(html)[0], icon: 'error' })
+ } else {
+
+ // 其他情况应该是系统挂了
+ swal('系统错误', '', 'error');
+ }
+ })
+ });
+```
+* laravel 中关于 http 请求响应状态码
+ * `200` => 响应成功,用 axios 请求接口,返回200,那么就会直接调用 `.then()`
+ * `401` => 响应失败,这是因为没有登陆,从中间件就排出去了,返回的状态吗可以通过 `error.response.status` 读取
+ * `422` => 响应失败,这是因为 AddCartRequest 校验数据失败
+ * `500` 以及其他状态码 => 这种问题通常是后台的问题,所以去查看控制器层的相关逻辑排错
+
+# 查看购物车
+* 添加方法 CartController@index
+```
+ /**
+ * 购物车列表
+ */
+ public function index(Request $request)
+ {
+ // $request->user() => 读取当前请求用户
+ // $cartItems() => 读取档期那用户的购物车
+ // with(['productSku.product']) => 防止 N+1 查询,查询当前购物车对应的商品 SKU 信息和商品信息
+ $cartItems = $request->user()->cartItems()->with(['productSku.product'])->get();
+
+ return view('cart.index', ['cartItems' => $cartItems]);
+ }
+```
+* 配置路由 `Route::get('cart', 'CartController@index')->name('cart.index'); //购物车信息`
+* 前端视图 (../cart/index.blade.php) 样式参考教程.
+* 最后在 ../layouts/_header.blade.php 中已登陆用户部分添加一个入口链接即可.
+
+# 将商品移除购物车
+* 添加删除方法 CartController@remove
+```
+use App\Models\ProductSku;
+
+...
+
+ /**
+ * 从购物车中删除
+ */
+ public function remove(ProductSku $sku, Request $request)
+ {
+ $request->user()->cartItems()->where('product_sku_id', $sku->id)->delete();
+
+ return [];
+ }
+```
+* 新增路由 `Route::delete('cart/{sku}', 'CartController@remove')->name('cart.remove'); //从购物车中删除商品`
+* 视图 ../cart/index.blade.php 中增加前端js逻辑代码
+```
+@section('scriptsAfterJs')
+
+@endsection
+```
\ No newline at end of file
diff --git a/laravel/laravel5/7.md b/laravel/laravel5/7.md
new file mode 100644
index 0000000..63227b7
--- /dev/null
+++ b/laravel/laravel5/7.md
@@ -0,0 +1,884 @@
+# 订单系统
+> 由于我们的一笔订单支持多个商品 SKU,因此我们需要 orders 和 order_items 两张表,orders 保存用户、金额、收货地址等信息,order_items 则保存商品 SKU ID、数量以及与 orders 表的关联.
+* 创建模型和数据表 `php artisan make:model Models/Order -m`, `php artisan make:model Models/OrderItem -m`
+ * orders 表的迁移
+ ```
+ $table->increments('id');
+ $table->string('no')->unique()->comments('流水号');
+ $table->unsignedInteger('user_id')->comments('购买用户');
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+ $table->text('address')->comments('收获地址 json 快照');
+ $table->decimal('total_amount')->comments('总价');
+ $table->text('remark')->nullable()->comments('备注');
+ $table->dateTime('paid_at')->nullable()->comments('支付时间');
+ $table->string('payment_method')->nullable()->comments('支付方式');
+ $table->string('payment_no')->nullable()->comments('支付平台订单号');
+ $table->string('refund_status')->default(\App\Models\Order::REFUND_STATUS_PENDING)->comments('退款状态');
+ $table->string('refund_no')->nullable()->comments('退款单号');
+ $table->boolean('closed')->default(false)->comments('订单是否关闭');
+ $table->boolean('reviewed')->default(false)->comments('订单是否评价');
+ $table->string('ship_status')->default(\App\Models\Order::SHIP_STATUS_PENDING)->comments('物流状态');
+ $table->text('ship_data')->nullable()->comments('物流数据');
+ $table->text('extra')->nullable()->comments('额外数据');
+ $table->timestamps();
+ ```
+ > 这里退款状态和物流状态都是读取的 Order 模型中的静态常量,详情参考下面模型处的代码
+ * order_items 表的迁移
+ ```
+ $table->increments('id');
+ $table->unsignedInteger('order_id')->comments('订单外键');
+ $table->foreign('order_id')->references('id')->on('orders')->onDelete('cascade');
+ $table->unsignedInteger('product_id')->comments('商品外键');
+ $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
+ $table->unsignedInteger('product_sku_id')->comments('商品 SKU 外键');
+ $table->foreign('product_sku_id')->references('id')->on('product_skus')->onDelete('cascade');
+ $table->unsignedInteger('amount')->comments('购买数量');
+ $table->decimal('price', 10, 2)->comments('商品单价');
+ $table->unsignedInteger('rating')->nullable()->comments('用户打分');
+ $table->text('review')->nullable()->comments('用户评价');
+ $table->timestamp('reviewed_at')->nullable()->comments('评价时间');
+ ```
+ * Order 模型
+ ```
+ /**
+ * 退款状态
+ */
+ const REFUND_STATUS_PENDING = 'pending';
+ const REFUND_STATUS_APPLIED = 'applied';
+ const REFUND_STATUS_PROCESSING = 'processing';
+ const REFUND_STATUS_SUCCESS = 'success';
+ const REFUND_STATUS_FAILED = 'failed';
+
+ /**
+ * 物流状态
+ */
+ const SHIP_STATUS_PENDING = 'pending';
+ const SHIP_STATUS_DELIVERED = 'delivered';
+ const SHIP_STATUS_RECEIVED = 'received';
+
+ /**
+ * 退款状态对应中文名称
+ */
+ public static $refundStatusMap = [
+ self::REFUND_STATUS_PENDING => '未退款',
+ self::REFUND_STATUS_APPLIED => '已申请退款',
+ self::REFUND_STATUS_PROCESSING => '退款中',
+ self::REFUND_STATUS_SUCCESS => '退款成功',
+ self::REFUND_STATUS_FAILED => '退款失败',
+ ];
+
+ /**
+ * 物流状态对应中文名称
+ */
+ public static $shipStatusMap = [
+ self::SHIP_STATUS_PENDING => '未发货',
+ self::SHIP_STATUS_DELIVERED => '已发货',
+ self::SHIP_STATUS_RECEIVED => '已收货',
+ ];
+
+ /**
+ * 可填字段
+ */
+ protected $fillable = [
+ 'no',
+ 'address',
+ 'total_amount',
+ 'remark',
+ 'paid_at',
+ 'payment_method',
+ 'payment_no',
+ 'refund_status',
+ 'refund_no',
+ 'closed',
+ 'reviewed',
+ 'ship_status',
+ 'ship_data',
+ 'extra',
+ ];
+
+ /**
+ * 字段数据类型自动转换
+ */
+ protected $casts = [
+ 'closed' => 'boolean',
+ 'reviewed' => 'boolean',
+ 'address' => 'json',
+ 'ship_data' => 'json',
+ 'extra' => 'json',
+ ];
+
+ /**
+ * 时间字段转换
+ */
+ protected $dates = [
+ 'paid_at',
+ ];
+
+ /**
+ * 引导函数
+ */
+ protected static function boot()
+ {
+ parent::boot();
+
+ // 监听模型创建事件,在写入数据库之前触发
+ static::creating(function ($model) {
+ // 如果模型的 no 字段为空
+ if (!$model->no) {
+ // 调用 findAvailableNo 生成订单流水号
+ $model->no = static::findAvailableNo();
+ // 如果生成失败,则终止创建订单
+ if (!$model->no) {
+ return false;
+ }
+ }
+ });
+ }
+
+ /**
+ * n:1 User
+ */
+ public function user()
+ {
+ return $this->belongsTo(User::class);
+ }
+
+ /**
+ * 1:n OrderItems
+ */
+ public function items()
+ {
+ return $this->hasMany(OrderItem::class);
+ }
+
+ /**
+ * 生成订单流水号
+ */
+ public static function findAvailableNo()
+ {
+ // 订单流水号前缀
+ $prefix = date('YmdHis');
+ for ($i = 0; $i < 10; $i++) {
+ // 随机生成 6 位的数字
+ $no = $prefix.str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
+ // 判断是否已经存在
+ if (!static::query()->where('no', $no)->exists()) {
+ return $no;
+ }
+ }
+ \Log::warning('find order no failed');
+
+ return false;
+ }
+ ```
+ * OrderItem 模型
+ ```
+ /**
+ * 可填字段
+ */
+ protected $fillable = ['amount', 'price', 'rating', 'review', 'reviewed_at'];
+
+ /**
+ * 字段自动转时间
+ */
+ protected $dates = ['reviewed_at'];
+
+ /**
+ * 创建和保存时不写入时间戳
+ */
+ public $timestamps = false;
+
+ /**
+ * n:1 Product
+ */
+ public function product()
+ {
+ return $this->belongsTo(Product::class);
+ }
+
+ /**
+ * n:1 ProductSku
+ */
+ public function productSku()
+ {
+ return $this->belongsTo(ProductSku::class);
+ }
+
+ /**
+ * n:1 Order
+ */
+ public function order()
+ {
+ return $this->belongsTo(Order::class);
+ }
+ ```
+
+# 生成订单
+1. 编辑 CartController@index => 在访问购物车页面的时,顺便把当前用户的收获地址也传过去,方便用户下单时选择收货地址
+```
+ /**
+ * 购物车列表
+ */
+ public function index(Request $request)
+ {
+ // $request->user() => 读取当前请求用户
+ // $cartItems() => 读取当前用户的购物车
+ // with(['productSku.product']) => 防止 N+1 查询,查询当前购物车对应的商品 SKU 信息和商品信息
+ $cartItems = $request->user()->cartItems()->with(['productSku.product'])->get();
+
+ // 获取收货地址
+ $addresses = $request->user()->addresses()->orderBy('last_used_at', 'desc')->get();
+
+ return view('cart.index', [
+ 'cartItems' => $cartItems,
+ 'addresses' => $addresses,
+ ]);
+ }
+```
+2. 在视图 ../cart/index.blade.php 中增加表单(省略)
+3. 创建控制器 `php artisan make:controller OrdersController`
+4. 创建请求类以验证数据 `php artisan make:request OrderRequest`,
+```
+ ['required', Rule::exists('user_addresses', 'id')->where('user_id', $this->user()->id)],
+ 'items' => ['required', 'array'],
+ 'items.*.sku_id' => [ // 检查 items 数组下每一个子数组的 sku_id 参数
+ 'required',
+ // 自定义校验规则($要验证的字段名, $字段值, $fail这里代指一个抛出错误 $errors 的函数)
+ function ($attribute, $value, $fail) {
+ // 找到商品 SKU 对象
+ $sku = ProductSku::find($value);
+ // 判断商品是否存在
+ if (!$sku) {
+ $fail('该商品不存在');
+ return;
+ }
+ // 判断商品是否在售
+ if (!$sku->product->on_sale) {
+ $fail('该商品未上架');
+ return;
+ }
+ // 判断是否还有货
+ if ($sku->stock === 0) {
+ $fail('该商品已售完');
+ return;
+ }
+ // 获取当前索引
+ preg_match('/items\.(\d+)\.sku_id/', $attribute, $m);
+ $index = $m[1];
+ // 根据索引找到用户所提交的购买数量
+ $amount = $this->input('items')[$index]['amount'];
+ // 判断用户要买的数量是否小于库存
+ if ($amount > 0 && $amount > $sku->stock) {
+ $fail('该商品库存不足');
+ return;
+ }
+ },
+ ],
+ 'items.*.amount' => ['required', 'integer', 'min:1'],
+ ];
+ }
+}
+```
+> 验证规则可以用数组的形式写 `'字段' => ['规则1', '规则2', ...]`
+
+> `Rule::exists('user_addresses', 'id')->where('user_id', $this->user()->id)` => 通过 `$this->user()->id` 获取发起请求的用户,以这个参数作为 where 条件,查询数据库,判断收获地址是否存在.
+
+> `items.*.sku_id` => 为 items 字段(是一个数组)下的所有元素中的 sku_id (这里 * 就代表所有元素) 制定校验规则.(最后会遍历一次 items 数组)
+
+> 自定义验证规则 `'字段' => ['规则1, function ($attribute, $value, $fail) {}']` => 第二个参数就是验证规则,它的三个参数: `$attribute` => 当前被验证的字段名(就是 '字段' => [...] 里面的 '字段' 这两个字), `$val` => 字段值, `$fail` => 应该是一个抛送错误提示信息的对象。
+
+> `$fail('xxx')` => 生成一条错误提示信息。
+
+> `preg_match('/items\.(\d+)\.sku_id/', $attribute, $m);` => 这里的 $arrtibute 就是遍历过程中的字段名 item.0.sku_id ,这里就是为了将 0 这个下标提取出来(存为 `$index` ),然后通过 `$this->input('items')[$index]['amount']` 来读取用户要购买的这个商品 SKU 的数量,再拿去比较,看看是否小于存库.
+
+1. 完成 OrdersController@store => 生成订单
+```
+user();
+ // 开启一个数据库事务
+ $order = \DB::transaction(function () use ($user, $request) {
+ // 读取收货地址
+ $address = UserAddress::find($request->input('address_id'));
+ // 更新此地址的最后使用时间
+ $address->update(['last_used_at' => Carbon::now()]);
+ // 创建一个订单
+ $order = new Order([
+ 'address' => [ // 将地址信息放入订单中
+ 'address' => $address->full_address,
+ 'zip' => $address->zip,
+ 'contact_name' => $address->contact_name,
+ 'contact_phone' => $address->contact_phone,
+ ],
+ 'remark' => $request->input('remark'),
+ 'total_amount' => 0,
+ ]);
+ // 订单关联到当前用户
+ $order->user()->associate($user);
+ // 写入数据库
+ $order->save();
+ // 此时创建了一个写有 用户id, 收获地址快照json(数组自动转成了 json), 购买备注, 以及总金额为0的一个订单.
+
+ // 现在开始处理 order_items 表的写入,完成之后更新订单总价
+ // 初始化订单总价
+ $totalAmount = 0;
+ // 获取提交的 SKU 信息
+ $items = $request->input('items');
+ // 遍历用户提交的 SKU
+ foreach ($items as $data) {
+ // 获取当前被遍历的 SKU
+ $sku = ProductSku::find($data['sku_id']);
+ // 创建一个 OrderItem 并直接与当前订单关联
+ $item = $order->items()->make([
+ 'amount' => $data['amount'], //写入数量
+ 'price' => $sku->price, //写入单价
+ ]);
+ // 关联商品id
+ $item->product()->associate($sku->product_id);
+ // 关联 SKU id
+ $item->productSku()->associate($sku);
+ // 保存一条数据到 order_items 表
+ $item->save();
+ // 增加总价
+ $totalAmount += $sku->price * $data['amount'];
+ // 减少库存,同时防止超卖
+ if ($sku->decreaseStock($data['amount']) <= 0) {
+ throw new InvalidRequestException('该商品库存不足');
+ }
+ }
+ // 此时根据订单信息,写入了多条订单详情数据到 order_items 表中
+
+ // 更新订单总金额
+ $order->update(['total_amount' => $totalAmount]);
+
+ // 将下单的商品从购物车中移除
+ $skuIds = collect($request->input('items'))->pluck('sku_id');
+ $user->cartItems()->whereIn('product_sku_id', $skuIds)->delete();
+
+ // 结束事务,返回 $order
+ return $order;
+ });
+
+ return $order;
+ }
+}
+```
+
+> `\DB::transaction()` => 开启数据库事务,接收一个闭包函数作参数,在闭包中可以对多张数据表进行操控,最后一起保存:在每一次生成订单的事务中,我们先操作 user_addresses 表(获取用户下单时选择的收获地址,然后更新该收获地址的最新使用时间), 然后操作 orders 表(写入一条新的订单数据:购买用户id, 收货地址json快照, 购买备注, 并且设置总金额为0), 然后遍历用户下单时传过来的商品SKU 信息(遍历每一条数据,写入 order_items 表:写入购买的商品, 具体的商品型号sku, 购买的数量, 单价. 同时每一次遍历结束前,计算叠加一次最新总价,减少库存), 然后再次操作 orders 表:更新之前创建的订单的总金额, 最后操作 cart_items 表(请阔购物车).
+
+> `$item = $order->items()->make([])` => 这里其实只是创建了一个对象,但是没有入库,最后 `$item->save()` 才是正式入库了.
+
+> `$item->product()->associate($sku->product_id);` 在正式入库前, 我们通过 `..->associate()` 写入外键.
+
+> 减少库存的方法 `$sku->decreaseStock($data['amount'])` 写在 ProductSku 模型中, 详情稍后说明, 这个函数的返回值是操作的数据库受影响的行数,所以最后判断是否写入成功(失败0条数据受影响)就是判断函数的返回值是否大于0
+
+> 如果减少库存操作出错,那么 `throw new InvalidRequestException('该商品库存不足');` => 抛出异常,且由于在数据库事务中执行,那么会直接回滚数据库到之前的状态.
+
+> `collect($request->input('items'))->pluck('sku_id');` => 遍历传递过来的 items ,读取里面的 sku_id, 返回值就是所有 sku_id 字段组成的数组.
+
+6. 关于减少/增加库存的方法,定义在 ProductSku 模型中
+```
+use App\Exceptions\InternalException; // <= 这里用内部报错
+
+...
+
+ /**
+ * 减少库存
+ */
+ public function decreaseStock($amount)
+ {
+ // 做一次判断:判断要减少的库存数量得大于0
+ if ($amount < 0) {
+ throw new InternalException('减库存不可小于0');
+ }
+
+ // 操作数据库:
+ return $this->newQuery()->where('id', $this->id)->where('stock', '>=', $amount)->decrement('stock', $amount);
+ }
+
+ /**
+ * 增加库存
+ */
+ public function addStock($amount)
+ {
+ if ($amount < 0) {
+ throw new InternalException('加库存不可小于0');
+ }
+ $this->increment('stock', $amount);
+ }
+```
+7. 配置路由 `Route::post('orders', 'OrdersController@store')->name('orders.store'); //生成订单`
+8. 完善前端逻辑 ../crat/index.blade.php
+```
+# 写在全选/反选后面
+
+ // 生成订单
+ $('.btn-create-order').click(function () {
+ // 构建请求参数,将用户选择的地址的 id 和备注内容写入请求参数
+ var req = {
+ address_id: $('#order-form').find('select[name=address]').val(),
+ items: [],
+ remark: $('#order-form').find('textarea[name=remark]').val(),
+ };
+ // 遍历
标签内所有带有 data-id 属性的 标签,也就是每一个购物车中的商品 SKU
+ $('table tr[data-id]').each(function () {
+ // 获取当前行的单选框
+ var $checkbox = $(this).find('input[name=select][type=checkbox]');
+ // 如果单选框被禁用或者没有被选中则跳过
+ if ($checkbox.prop('disabled') || !$checkbox.prop('checked')) {
+ return;
+ }
+ // 获取当前行中数量输入框
+ var $input = $(this).find('input[name=amount]');
+ // 如果用户将数量设为 0 或者不是一个数字,则也跳过
+ if ($input.val() == 0 || isNaN($input.val())) {
+ return;
+ }
+ // 把 SKU id 和数量存入请求参数数组中
+ req.items.push({
+ sku_id: $(this).data('id'),
+ amount: $input.val(),
+ })
+ });
+ axios.post('{{ route('orders.store') }}', req)
+ .then(function (response) {
+ swal('订单提交成功', '', 'success')
+ .then(() => {
+ location.href = '/orders/' + response.data.id;
+ });
+ }, function (error) {
+ if (error.response.status === 422) {
+ // http 状态码为 422 代表用户输入校验失败
+ var html = '';
+ _.each(error.response.data.errors, function (errors) {
+ _.each(errors, function (error) {
+ html += error + '
';
+ })
+ });
+ html += '
';
+ swal({ content: $(html)[0], icon: 'error' })
+ } else {
+ // 其他情况应该是系统挂了
+ swal('系统错误', '', 'error');
+ }
+ });
+ });
+```
+
+# 关闭未支付的订单
+> 下单完成后,无论用户支付还是没支付,都会导致库存减少。恶意用户可以通过无限下单,但不支付,导致商品库存被占,其他用户买不了。为了杜绝这一情况的发生,用延时任务完成
+1. 为了使用 redis ,安装 predis 扩展
+ * 命令 `composer require predis/predis`
+ * 编辑配置文件 .env
+ ```
+ QUEUE_DRIVER=redis
+ ```
+2. 创建任务 `php artisan make:job CloseOrder` => 创建的文件位于 app/Jobs
+```
+order = $order;
+ // 设置延迟的时间,delay() 方法的参数代表多少秒之后执行
+ $this->delay($delay);
+ }
+
+ // 定义这个任务类具体的执行逻辑
+ // 当队列处理器从队列中取出任务时,会调用 handle() 方法
+ public function handle()
+ {
+ // 判断对应的订单是否已经被支付
+ // 如果已经支付则不需要关闭订单,直接退出
+ if ($this->order->paid_at) {
+ return;
+ }
+ // 通过事务执行 sql
+ \DB::transaction(function() {
+ // 将订单的 closed 字段标记为 true,即关闭订单
+ $this->order->update(['closed' => true]);
+ // 循环遍历订单中的商品 SKU,将订单中的数量加回到 SKU 的库存中去
+ foreach ($this->order->items as $item) {
+ $item->productSku->addStock($item->amount);
+ }
+ });
+ }
+}
+```
+3. 在 OrdersController@store 方法中触发这个任务
+```
+use App\Jobs\CloseOrder;
+
+...
+
+ // 写在最后 return $order 之前
+ // 开始延时计划任务:在一段时间后关闭订单
+ $this->dispatch(new CloseOrder($order, config('app.order_ttl')));
+```
+> 这里的一段时间定义在 config/app.php 中 `'order_ttl' => 30,` (单位秒),方便测试这里写30秒,以后根据需要设置时间即可。
+4. 测试
+ * 开启队列任务监视 `php artisan queue:work`
+ * 然后在网页中,打开某个商品的详情,记住数量,添加到购物车,下单,此时数量减少,然后注意控制台,当出现
+ ```
+ [执行时间] Processing: App\Jobs\CloseOrder
+ [执行时间] Processed: App\Jobs\CloseOrder
+ ```
+ > 此时说明订单已经关闭,订单对应的商品的数量回到之前的数量。
+
+# 用户订单列表
+1. 控制器 OrdersController@index
+```
+ /**
+ * 用户订单列表
+ */
+ public function index(Request $request)
+ {
+ $orders = Order::query()
+ ->where('user_id', $request->user()->id) //查询当前用户的订单
+ ->orderBy('created_at', 'desc') //根据创建时间排序
+ ->with(['items.product', 'items.productSku']) //顺便把商品 SKU 信息查出来防止 N+1 查询
+ ->paginate();
+
+ return view('orders.index', ['orders' => $orders]);
+ }
+```
+2. 路由 `Route::get('orders', 'OrdersController@index')->name('orders.index'); //用户订单列表`
+3. 视图( ../orders/index.blade.php )详情和样式参考教程,唯一需要注意的就是订单状态部分
+```
+@if($order->paid_at)
+ @if($order->refund_status === \App\Models\Order::REFUND_STATUS_PENDING)
+ 已支付
+ @else
+ {{ \App\Models\Order::$refundStatusMap[$order->refund_status] }}
+ @endif
+@elseif($order->closed)
+ 已关闭
+@else
+ 未支付
+
请于 {{ $order->created_at->addSeconds(config('app.order_ttl'))->format('H:i') }} 前完成支付
+
否则订单将自动关闭
+@endif
+```
+> `$order->created_at->addSeconds(config('app.order_ttl'))->format('H:i')` => `$order->created_at` 订单创建时间, `addSeconds(config('app.order_ttl'))` 增加秒数, `->format('H:i')` 变为 “小时:分钟” 的格式。(最后显示的就是订单关闭的时间)
+4. 增加入口链接“我的订单” ../layouts/_header.blade.php(略)
+
+# 用户订单详情
+1. 控制器 OrdersController@show
+```
+ /**
+ * 用户订单详情
+ */
+ public function show(Order $order, Request $request)
+ {
+ return view('orders.show', ['order' => $order->load(['items.productSku', 'items.product'])]);
+ }
+```
+2. 路由 `Route::get('orders/{order}', 'OrdersController@show')->name('orders.show'); //用户订单详情`
+3. 视图 ( ../orders/show.blade.php ),略
+4. 增加入口链接 ../orders/index.blade.php,略
+5. 确保当前用户只能查看自己的订单
+ * 创建授权策略类 `php artisan make:policy OrderPolicy --model=Models/Order`
+ ```
+ /**
+ * 确保操作的订单是自己的
+ */
+ public function own(User $user, Order $order)
+ {
+ return $order->user_id == $user->id;
+ }
+ ```
+ * 注册授权策略类 app/Providers/AuthServiceProvider.php
+ ```
+ protected $policies = [
+ 'App\Model' => 'App\Policies\ModelPolicy',
+ \App\Models\UserAddress::class => \App\Policies\UserAddressPolicy::class,
+ \App\Models\Order::class => \App\Policies\OrderPolicy::class,
+ ];
+ ```
+ * 在控制器层 OrdersController@show 中授权 `$this->authorize('own', $order);`
+
+# 封装业务代码
+> 我们已经完成了购物车功能和下单功能,但是我们会发现我们在 Controller 里面写了大量的包含复杂逻辑的业务代码,这是一个坏习惯,这样子随着需求的增加,我们的控制器很快就变得臃肿。如果以后我们要开发 App 端,这些代码可能需要在 Api 的 Controller 里再重复一遍,假如出现业务逻辑的调整就需要修改两个或更多地方,这明显是不合理的。因此我们需要对 逻辑复杂 的 业务代码 进行封装。
+* 创建一个文件夹 app/Services => 用于存放相关业务逻辑的封装代码文件
+* 购物车 app/Services/CartService.php
+```
+cartItems()->with(['productSku.product'])->get();
+ }
+
+ /**
+ * 添加商品
+ */
+ public function add($skuId, $amount)
+ {
+ $user = Auth::user();
+ // 从数据库中查询该商品是否已经在购物车中
+ if ($item = $user->cartItems()->where('product_sku_id', $skuId)->first()) {
+ // 如果存在则直接叠加商品数量
+ $item->update([
+ 'amount' => $item->amount + $amount,
+ ]);
+ } else {
+ // 否则创建一个新的购物车记录
+ $item = new CartItem(['amount' => $amount]);
+ $item->user()->associate($user);
+ $item->productSku()->associate($skuId);
+ $item->save();
+ }
+
+ return $item;
+ }
+
+ /**
+ * 删除商品
+ */
+ public function remove($skuIds)
+ {
+ // 可以传单个 ID,也可以传 ID 数组
+ if (!is_array($skuIds)) {
+ $skuIds = [$skuIds];
+ }
+ Auth::user()->cartItems()->whereIn('product_sku_id', $skuIds)->delete();
+ }
+}
+```
+* 改写 CartController
+```
+cartService = $cartService;
+ }
+
+ /**
+ * 用户购物车列表
+ */
+ public function index(Request $request)
+ {
+ $cartItems = $this->cartService->get();
+ $addresses = $request->user()->addresses()->orderBy('last_used_at', 'desc')->get();
+
+ return view('cart.index', ['cartItems' => $cartItems, 'addresses' => $addresses]);
+ }
+
+ /**
+ * 购物车:添加商品
+ */
+ public function add(AddCartRequest $request)
+ {
+ $this->cartService->add($request->input('sku_id'), $request->input('amount'));
+
+ return [];
+ }
+
+ /**
+ * 购物车:减少商品
+ */
+ public function remove(ProductSku $sku, Request $request)
+ {
+ $this->cartService->remove($sku->id);
+
+ return [];
+ }
+}
+```
+> 1, `use App\Services\CartService;` => 引用我们封装的操作类
+> 2, 定义一个变量 `protected $cartService;` 然后在构造函数的参数列表中实例化上面的操作类 `public function __construct(CartService $cartService)` 在内部将这个操作类赋给 `$this->cartService = $cartService`, 此时就可以用 $this->cartService 来调用我们写在 app/Services/CartService.php 中的方法来操作购物车了。
+
+* 改写 OrdersController@store
+```
+use App\Services\CartService;
+
+ /**
+ * 生成订单
+ */
+ public function store(OrderRequest $request, CartService $cartService)
+ {
+ ...
+
+ // 开启一个数据库事务
+ $order = \DB::transaction(function () use ($user, $request) {
+ ...
+
+ // 将下单的商品从购物车中移除
+ $skuIds = collect($request->input('items'))->pluck('sku_id')->all();
+ $cartService->remove($skuIds);
+
+ ...
+ });
+
+ ...
+ }
+```
+> 为什么在 CartController 里面要实例化,而在 OrdersController 中在方法参数列表中实例化的原因是因为 CartController 每个方法都要用 CartService 提供的方法,而 OrdersController 中就只在生成订单的方法中,生成完毕后清空购物车中。
+---------------------------------------------------------------------------------
+* 封装 OrdersController 中的相关业务代码,首先新建一个 app/Services/OrderService.php
+```
+update(['last_used_at' => Carbon::now()]);
+ // 创建一个订单
+ $order = new Order([
+ 'address' => [ // 将地址信息放入订单中
+ 'address' => $address->full_address,
+ 'zip' => $address->zip,
+ 'contact_name' => $address->contact_name,
+ 'contact_phone' => $address->contact_phone,
+ ],
+ 'remark' => $remark,
+ 'total_amount' => 0,
+ ]);
+ // 订单关联到当前用户
+ $order->user()->associate($user);
+ // 写入数据库
+ $order->save();
+
+ $totalAmount = 0;
+ // 遍历用户提交的 SKU
+ foreach ($items as $data) {
+ $sku = ProductSku::find($data['sku_id']);
+ // 创建一个 OrderItem 并直接与当前订单关联
+ $item = $order->items()->make([
+ 'amount' => $data['amount'],
+ 'price' => $sku->price,
+ ]);
+ $item->product()->associate($sku->product_id);
+ $item->productSku()->associate($sku);
+ $item->save();
+ $totalAmount += $sku->price * $data['amount'];
+ if ($sku->decreaseStock($data['amount']) <= 0) {
+ throw new InvalidRequestException('该商品库存不足');
+ }
+ }
+ // 更新订单总金额
+ $order->update(['total_amount' => $totalAmount]);
+
+ // 将下单的商品从购物车中移除
+ $skuIds = collect($items)->pluck('sku_id')->all();
+ app(CartService::class)->remove($skuIds);
+
+ return $order;
+ });
+
+ // 这里我们直接使用 dispatch 函数
+ dispatch(new CloseOrder($order, config('app.order_ttl')));
+
+ return $order;
+ }
+}
+```
+> 1,在之前的写法中,我们是直接用 `$request->user()` 来读取发起请求的用户,但是要注意 **Request 不可以出现在控制器和中间件以外的地方**
+
+> 2,这里直接用 `app(CartService::class)` 来实例化了 CartService 这个我们在上一步封装的购物车操作类。在我们代码里调用封装的库时一定 **不可以** 使用 new 关键字来初始化,而是应该通过 Laravel 的容器来初始化,因为在之后的开发过程中 CartService 类的构造函数可能会发生变化,比如注入了其他的类,如果我们使用 new 来初始化的话,就需要在每个调用此类的地方进行修改;而使用 app() 或者自动解析注入等方式 Laravel 则会自动帮我们处理掉这些依赖。(说白了你要 new 的话可能由于你 new 的这个 CartService 内部还 new 了其他不少类,导致项目运行效率大打折扣)
+
+> 3,这里的 `dispatch(new CloseOrder($order, config('app.order_ttl')))` 是一个全局助手函数(控制器里面可以用 `$this->dispatch() 调用`,但是这里不行,所以 Laravel 给我们提供了这么一个全局函数,直接调就行)
+
+* 最后处理 OrdersController.php
+```
+ /**
+ * 生成订单
+ */
+ public function store(OrderRequest $request, OrderService $orderService)
+ {
+ $user = $request->user();
+ $address = UserAddress::find($request->input('address_id'));
+
+ return $orderService->store($user, $address, $request->input('remark'), $request->input('items'));
+ }
+```
+> 接上面注意事项的第1点:因为不能在 OrderService 里面通过 `$request->user()` 读发起请求的用户,所以只好在控制器中先读好 `$user = $request->user();` 然后再作为实参传给 OrderService 中的 store() 方法。
+
+* Laravel - Service 模型的 [参考文档](https://oomusou.io/laravel/service/)
\ No newline at end of file
diff --git a/laravel/laravel5/8.md b/laravel/laravel5/8.md
new file mode 100644
index 0000000..b9853c3
--- /dev/null
+++ b/laravel/laravel5/8.md
@@ -0,0 +1,815 @@
+# 微信和支付宝支付扩展
+* [yansongda/pay](https://yansongda.gitbooks.io/pay/) 安装命令 `composer require yansongda/pay`
+* 创建一个配置文件 config/pay.php
+```
+ [
+ 'app_id' => '',
+ 'ali_public_key' => '',
+ 'private_key' => '',
+ 'log' => [
+ 'file' => storage_path('logs/alipay.log'),
+ ],
+ ],
+
+ /**
+ * 微信支付需要的参数
+ */
+ 'wechat' => [
+ 'app_id' => '',
+ 'mch_id' => '',
+ 'key' => '',
+ 'cert_client' => '',
+ 'cert_key' => '',
+ 'log' => [
+ 'file' => storage_path('logs/wechat_pay.log'),
+ ],
+ ],
+];
+```
+> 相关参数在后续开发中配置
+* 注入容器:编辑 app/Providers/AppServiceProvider@register
+```
+use Monolog\Logger;
+use Yansongda\Pay\Pay;
+
+...
+
+ /**
+ * 注册服务容器对象
+ */
+ public function register()
+ {
+ // 注入 alipay 对象
+ $this->app->singleton('alipay', function () {
+ // 获取配置文件信息(config/pay.php 中的 alipay 数组的信息)
+ $config = config('pay.alipay');
+ // 判断当前项目运行环境是否为测试环境
+ if (app()->environment() !== 'production') {
+ $config['mode'] = 'dev'; //设置配置为开发模式
+ $config['log']['level'] = Logger::DEBUG; //以 debug 级别记录日志
+ } else { //否则为生产环境(项目在上线运营)
+ $config['log']['level'] = Logger::WARNING; //否则以 warning 级别记录日志
+ }
+ // 返回一个实例化 alipay 支付宝支付对象(由 yansongda/pay 扩展提供)
+ return Pay::alipay($config);
+ });
+
+ // 注入 wechat_pay 对象
+ $this->app->singleton('wechat_pay', function () {
+ // 获取配置文件信息
+ $config = config('pay.wechat');
+ // 判断当前项目是否为测试环境
+ if (app()->environment() !== 'production') {
+ $config['log']['level'] = Logger::DEBUG;
+ // 因为微信支付没有 dev 开发模式,所以这里之配置日志即可
+ } else {
+ $config['log']['level'] = Logger::WARNING;
+ }
+ // 返回一个实例化 wechat 微信支付对象
+ return Pay::wechat($config);
+ });
+ }
+```
+* 测试:进入 tinker ,调用 `app('alipay')` 或者 `app('wechat_pay')` 看看是否能够成功的实例化支付对象。(在项目开发中,随时可以调用全局函数 `app('alipay')` 等直接实例化一个支付对象。)
+
+# 支付宝支付
+* 访问 [生成密钥方法](https://opensupport.alipay.com/support/knowledge/20069/201602048385?ant_source=zsearch) 选择合适的版本下载密钥生成工具
+ * 下好之后解压打开,选择密钥格式为 PKCS1, 长度 2048, 点击生成密钥
+* 注册支付宝[沙箱应用](https://openhome.alipay.com/platform/appDaily.htm?tab=info) => 一个假的支付接口,用于开发测试
+ * 点击必看部分中的 “设置应用公钥”
+ * 点击弹窗中的 “设置应用公钥” => 将密钥生成工具生成的公钥复制到里面,点保存
+ * 点击查看支付宝公钥,确认是否设置成功
+------------------------------------------------------------
+* 编辑 config/pay.php
+```
+'app_id' => '支付宝沙箱应用网页上的 APPID',
+'ali_public_key' => '支付宝沙箱应用网页上的 支付宝公钥',
+'private_key' => '本地密钥生成工具生成的私钥',
+```
+------------------------------------------------------------
+* 测试:
+ * 增加一条测试路由 routes/web.php
+ ```
+ // 支付测试
+ Route::get('alipay', function() {
+ return app('alipay')->web([
+ 'out_trade_no' => time(), //订单流水
+ 'total_amount' => '1', //订单金额(单位元)
+ 'subject' => 'test subject - 测试', //订单标题
+ ]);
+ });
+ ```
+ * 访问 `项目网站/alipay` => 会自动跳转到支付宝支付页面,支付的账号密码可以在 [沙箱账号](https://openhome.alipay.com/platform/appDaily.htm?tab=account) 中查看,最后可以完成一次支付。
+--------------------------------------------------------------
+
+# 完成支付宝支付功能
+* 创建一个支付控制器 `php artisan make:controller PaymentController`
+```
+use App\Models\Order;
+use App\Exceptions\InvalidRequestException;
+
+...
+
+ /**
+ * 跳转到支付宝支付页面
+ */
+ public function payByAlipay(Order $order, Request $request)
+ {
+ // 判断订单是否属于当前用户
+ $this->authorize('own', $order);
+
+ // 订单已支付或者已关闭
+ if ($order->paid_at || $order->closed) {
+ throw new InvalidRequestException('订单状态不正确');
+ }
+
+ // 调用支付宝的网页支付
+ return app('alipay')->web([
+ 'out_trade_no' => $order->no, // 订单编号,需保证在商户端不重复
+ 'total_amount' => $order->total_amount, // 订单金额,单位元,支持小数点后两位
+ 'subject' => '支付 Laravel Shop 的订单:'.$order->no, // 订单标题
+ ]);
+ }
+
+ /**
+ * 支付宝-前端回调(浏览器)
+ */
+ public function alipayReturn()
+ {
+ // 校验提交的参数是否合法
+ $data = app('alipay')->verify();
+ dd($data);
+ }
+
+ /**
+ * 支付宝-后端回调(数据库)
+ */
+ public function alipayNotify()
+ {
+ $data = app('alipay')->verify();
+ \Log::debug('Alipay notify', $data->all());
+ }
+```
+> **前端回调** 当用户支付成功之后支付宝会让用户浏览器跳转回项目页面并带上支付成功的参数,也就是说前端回调依赖于用户浏览器,**如果用户在跳转之前关闭浏览器,将无法收到前端回调**。
+
+> `app('alipay')->verify()` => 验证提交的参数是否合法:支付宝的前端跳转会带有数据签名,通过校验数据签名可以判断参数是否被恶意用户篡改。同时该方法还会返回解析后的参数。
+
+> **后端回调** 支付成功之后支付宝的服务器会用订单相关数据作为参数请求项目的接口,不依赖用户浏览器。(支付宝请求我们项目的某个方法)
+
+> `\Log::debug('Alipay notify', $data->all());` => 因为不能直接 `dd()` 打印服务器端回调接收的参数,所以需要通过日志的方式来保存
+------------------------------------------------------------------
+* 配置路由 routes/web.php
+```
+Route::group(['middleware' => 'email_verified'], function() {
+
+ ...
+
+ Route::get('payment/{order}/alipay', 'PaymentController@payByAlipay')->name('payment.alipay'); //支付宝支付
+ Route::get('payment/alipay/return', 'PaymentController@alipayReturn')->name('payment.alipay.return'); //支付宝-前端回调
+});
+Route::post('payment/alipay/notify', 'PaymentController@alipayNotify')->name('payment.alipay.notify');// 支付宝-后端回调
+```
+> 这里前端回调要写在已登陆且已验证邮箱的用户可以访问的路由组中,而后端回调不可以。
+* 编辑 app/Providers/AppServiceProvider@register => 设置回调路由
+```
+ // 注入 alipay 对象
+ $this->app->singleton('alipay', function () {
+ // 获取配置文件信息(config/pay.php 中的 alipay 数组的信息)
+ $config = config('pay.alipay');
+
+ // **设置回调路由**
+ $config['notify_url'] = route('payment.alipay.notify'); //后端回调
+ $config['return_url'] = route('payment.alipay.return'); //前端回调
+
+ // 判断当前项目运行环境是否为测试环境
+ if (app()->environment() !== 'production') {
+ $config['mode'] = 'dev'; //设置配置为开发模式
+ $config['log']['level'] = Logger::DEBUG; //以 debug 级别记录日志
+ } else { //否则为生产环境(项目在上线运营)
+ $config['log']['level'] = Logger::WARNING; //否则以 warning 级别记录日志
+ }
+ // 返回一个实例化 alipay 支付宝支付对象(由 yansongda/pay 扩展提供)
+ return Pay::alipay($config);
+ });
+```
+> 这里可以用 `route('路由名称')` => 来读取我们的前后端回调的接口路由。
+-------------------------------------------------------------
+* 测试
+> 这里有个问题需要注意:我们的项目在本地开发,在浏览器环境下,支付成功后,可以完成前端回调,但是后端不行(因为服务器没在互联网上),所以需要一些特别的手段:
+* [requestbin](https://requestbin.fullcontact.com/) => 是一个免费开源的网站,任何人都可以在上面申请一个专属的 URL(通常有效期 48 小时),对这个 URL 的任何类型的请求都会被记录下来,URL 的创建者可以看到请求的具体信息,包含请求时间、请求头、请求具体数据等。
+ * 打开后点击页面上的 “Create a RequestBin” => 就创建了一台可以接收支付宝后端回调信息的的服务器。
+ * 然后复制 “Bin URL” ,编辑 app/Providers/AppServiceProvider@register 支付宝对象注入那一部分, 把后端回调地址改成(别关闭)
+ ```
+ $config['notify_url'] = '新开的 Bin URL'; // <=后端回调
+ ```
+ * 此时再下单、支付一次,前端回调将打印支付宝回传过来的信息,而后端信息可以在 RequestBin 网页中右上角的圆点处打开查看.
+ * 注意此时页面中的 RAW BODY 这一段代码,复制它,然后在控制台中输入 `curl -XPOST http://shop.test/payment/alipay/notify -d'复制 raw body 内容'` => 用 curl 请求这个钩子,看看具体的内容
+ * 但其实此时仍然会出错,输入命令之后会返回给我们一个 html 文本信息, title是 **Page Expired** => 原因是因为这个请求动作没有带上 csrf 认证,因此我们需要在 app/Http/Middleware/VerifyCsrfToken.php 中配置:禁用对这个钩子的 csrf 认证
+ ```
+ /**
+ * 取消 csrf 认证
+ */
+ protected $except = [
+ 'payment/alipay/notify', // <= 当请求这个 url 时,不需要 csrf 认证.
+ ];
+ ```
+ * 再次输入 curl 命令请求,会返回空,因为 PaymentController@alipayNotify 方法,即后端回调方法,是记录日志的形式接收支付宝回调钩子的具体信息的: `\Log::debug('Alipay notify', $data->all());`
+ * 那么此时输入 `less storage/logs/laravel.log` => 查看 laravel 日志,按快捷键 shift + P 即可跳到最后面查看支付宝支付成功的信息,大概是这样的.
+ ```
+ Alipay notify {"gmt_create":"2018-08-28 13:40:49","charset":"GBK","gmt_payment":"2018-08-28 13:40:58","notify_time":"2018-08-28 13:45:37","subject":"支付 Laravel Shop 的订单:20180828054030470977","sign":"Z84wtT+PxXwKoCZf84rK9K5696B1SY7/2XuyJHBS1dZ3u+/4O+CBEmfJ2r9IO7GULcGy+x0lg64G5+cd93TsJqjrUZCrnltzfCWUYFniTVWo/XMi54s5YmKLjz9zsynj8neDNPuP9bQ3jNO8BFNVP2ejqbftkbOzKQE9mcYln1uT8G4d4f/eht2rITib20+I7grO0yMXCORwNRxCYv3PRuED9lOoA9vmh/dS06vvmIHoYuFPgD/4dpGuZjz0KFA19JjmaCG5RjpuNuRg9sCKblJzNr5mXkDhC+GmQpDV8khT8sQ5IgjLaXFeUX07X0whxirDkSKAlvyy0/d5sukpUA==","buyer_id":"2088102176547557","invoice_amount":"562.00","version":"1.0","notify_id":"0406353a7930a33f9ad174578aeacd2k8u","fund_bill_list":"[{\"amount\":\"562.00\",\"fundChannel\":\"ALIPAYACCOUNT\"}]","notify_type":"trade_status_sync","out_trade_no":"20180828054030470977","total_amount":"562.00","trade_status":"TRADE_SUCCESS","trade_no":"2018082821001004550200676168","auth_app_id":"2016091600524507","receipt_amount":"562.00","point_amount":"0.00","app_id":"2016091600524507","buyer_pay_amount":"562.00","sign_type":"RSA2","seller_id":"2088102175885689"}
+ ```
+ > 如果上面的信息无误,按 Q 退出查看日志,此时前端回调(支付完成后浏览器页面返回我们的项目),以及后端回调(支付完成后支付宝隔一段时间就会自动请求我们的后端接口一次)
+
+* 完成前端,后端回调
+```
+ /**
+ * 支付宝-前端回调(浏览器)
+ */
+ public function alipayReturn()
+ {
+ // 校验数据
+ try {
+ app('alipay')->verify();
+ } catch (\Exception $e) { //如果出错则抛出异常
+ return view('pages.error', ['msg' => '数据不正确']);
+ }
+
+ // 否则就跳转到通知页面告诉用户支付成功
+ return view('pages.success', ['msg' => '付款成功']);
+ }
+
+ /**
+ * 支付宝-后端回调(修改数据库,通知支付宝不用继续请求这个接口了)
+ */
+ public function alipayNotify()
+ {
+ // 校验输入参数
+ $data = app('alipay')->verify();
+ // $data->out_trade_no 拿到订单流水号,并在数据库中查询
+ $order = Order::where('no', $data->out_trade_no)->first();
+ // 正常来说不太可能出现支付了一笔不存在的订单,这个判断只是加强系统健壮性。
+ if (!$order) {
+ return 'fail';
+ }
+ // 如果这笔订单的状态已经是已支付
+ if ($order->paid_at) {
+ // 返回数据给支付宝
+ return app('alipay')->success();
+ }
+
+ // 修改订单状态
+ $order->update([
+ 'paid_at' => Carbon::now(), // 支付时间
+ 'payment_method' => 'alipay', // 支付方式
+ 'payment_no' => $data->trade_no, // 支付宝订单号
+ ]);
+
+ // 告诉支付宝,不用再继续请求我们的接口了,我们已经收到了用户成功地通过支付宝支付的信息,且更新了我们的数据库了
+ return app('alipay')->success();
+ }
+```
+> 此时后端回调还是要用 requestBin 进行处理.
+
+# 支付宝支付的总结
+1. 支付流程:
+ * 用户生成订单
+ * 拉起支付 => 当用户点击支付宝支付时 , 请求支付页面 PaymentController@payByAlipay , 会跳转到支付宝的支付页面.
+ * 在支付宝支付的页面不用我们处理 , 那是支付宝里面的事
+ * 在用户支付完成后, 支付宝那边会发送两个请求到我们这边: 前端回调(回到我们的支付成功提示页面), 后端回调(告诉后台已经支付成功)
+ * 前端回调我们只需要抛出一个提示信息,告诉用户:已经支付成功.
+ * 后端回调我们则需要更新数据库,同时告诉支付宝,我们收到了用户支付成功的消息,不用再请求我们的后端回调方法了(支付宝没有收到我们成功的消息会一直隔一段时间发送请求)
+ * 在支付宝请求我们的后端回调方法时,是不带 csrf 认证 token 的,目前的解决方法就是配置 app/Http/Middleware/VerifyCsrfToken@`$except` => 添加我们的后端接口 url,告诉 laravel 当别人请求这个地址的时候,不用校验是否有 csrf_token
+2. 具体支付操作
+ * 实例化支付宝对象
+ 1. 我们将不会改变的配置信息写在文件 config/pay.php 中
+ ```
+ /**
+ * 支付宝支付需要的参数
+ */
+ 'alipay' => [
+ 'app_id' => '支付宝中注册的应用id',
+ 'ali_public_key' => '支付公钥',
+ 'private_key' => '服务器私钥',
+ 'log' => [ //日志
+ 'file' => storage_path('logs/alipay.log'),
+ ],
+ ]
+ ```
+ 2. 我们将可能会改变的信息和实例化支付对象的过程写在 app/Providers/AppServiceProvider.php 中
+ ```
+ // 注入 alipay 对象
+ $this->app->singleton('alipay', function () {
+ // 获取配置文件信息(config/pay.php 中的 alipay 数组的信息)
+ $config = config('pay.alipay');
+ // 设置回调路由
+ $config['notify_url'] = 'http://requestbin.fullcontact.com/1f0uvg61'; //后端回调
+ $config['return_url'] = route('payment.alipay.return'); //前端回调
+ // 判断当前项目运行环境是否为测试环境
+ if (app()->environment() !== 'production') {
+ $config['mode'] = 'dev'; //设置配置为开发模式
+ $config['log']['level'] = Logger::DEBUG; //以 debug 级别记录日志
+ } else { //否则为生产环境(项目在上线运营)
+ $config['log']['level'] = Logger::WARNING; //否则以 warning 级别记录日志
+ }
+ // 返回一个实例化 alipay 支付宝支付对象(由 yansongda/pay 扩展提供)
+ return Pay::alipay($config);
+ });
+ ```
+ > `$config = config('pay.alipay')` => 读取 config/pay.php 中的 alipay 数组的信息(不经常改变的支付宝支付配置信息), 存储在 $config 变量中
+
+ > `$config['notify_url']` 和 `$config['return_url']` 这是在继续完成配置信息:配置后端回调的地址和前端回调的地址
+
+ > `app()->enviroment()` => 如果项目是生产环境已投入运营,那么这个函数的返回值是 `'production'`,我们判断这个函数的返回值,看看项目的运行环境,根据生产环境的不同我们需要给支付宝支付对象配置一些不同的信息,比如 `$config['mode']` 参数.(生产环境在 .env `APP_ENV=local` 这一参数中修改)
+
+ > `Pay::alipay($config)` => 这是在实例化一个由 yansongda/pay 扩展提供的支付宝支付对象(里面为我们封装了拉起支付等方法,只需要我们传指定参数即可)
+ * 拉起支付(展示支付页面):
+ ```
+ /**
+ * 跳转到支付宝支付页面
+ */
+ public function payByAlipay(Order $order, Request $request)
+ {
+ // 判断订单是否属于当前用户
+ $this->authorize('own', $order);
+
+ // 订单已支付或者已关闭
+ if ($order->paid_at || $order->closed) {
+ throw new InvalidRequestException('订单状态不正确');
+ }
+
+ // 调用支付宝的网页支付
+ return app('alipay')->web([
+ 'out_trade_no' => $order->no, // 订单编号,需保证在商户端不重复
+ 'total_amount' => $order->total_amount, // 订单金额,单位元,支持小数点后两位
+ 'subject' => '支付 Laravel Shop 的订单:'.$order->no, // 订单标题
+ ]);
+ }
+ ```
+ > 前两步无需多言,就是验证一下订单是否可用
+
+ > 调用支付宝的网页支付过程中, 首先 `app('alipay')` => 通过注入服务容器,我们直接用这个函数实例化了一个支付宝支付的对象
+
+ > `..->web()` => 接收一个数组作为参数,数组需要给3个值: 订单编号 `out_trade_no`, 总金额 `total_amount`, 订单标题 `subject` , 这些参数都可以从当前订单中读取
+
+ * 回调函数的处理
+ ```
+ /**
+ * 支付宝-前端回调(浏览器)
+ */
+ public function alipayReturn()
+ {
+ // 校验数据
+ try {
+ app('alipay')->verify();
+ } catch (\Exception $e) { //如果出错则抛出异常
+ return view('pages.error', ['msg' => '数据不正确']);
+ }
+
+ // 否则就跳转到通知页面告诉用户支付成功
+ return view('pages.success', ['msg' => '付款成功']);
+ }
+
+ /**
+ * 支付宝-后端回调(修改数据库,通知支付宝不用继续请求这个接口了)
+ */
+ public function alipayNotify()
+ {
+ // 校验输入参数
+ $data = app('alipay')->verify();
+ // $data->out_trade_no 拿到订单流水号,并在数据库中查询
+ $order = Order::where('no', $data->out_trade_no)->first();
+ // 正常来说不太可能出现支付了一笔不存在的订单,这个判断只是加强系统健壮性。
+ if (!$order) {
+ return 'fail';
+ }
+ // 如果这笔订单的状态已经是已支付
+ if ($order->paid_at) {
+ // 返回数据给支付宝
+ return app('alipay')->success();
+ }
+
+ // 修改订单状态
+ $order->update([
+ 'paid_at' => Carbon::now(), // 支付时间
+ 'payment_method' => 'alipay', // 支付方式
+ 'payment_no' => $data->trade_no, // 支付宝订单号
+ ]);
+
+ // 告诉支付宝,不用再继续请求我们的接口了,我们已经收到了用户成功地通过支付宝支付的信息,且更新了我们的数据库了
+ return app('alipay')->success();
+ }
+ ```
+ > 前端回调是在浏览器中进行的,后端回调是在互联网上(支付宝服务器和我们服务器的后台互相沟通)进行的.
+
+ > `app('alipay')->verify();` => 校验请求回调的参数(支付宝服务器请求我们的服务器,回调一定是正确的,这一步是为了防止恶意用户请求我们的两个回调方法)
+
+ > `return app('alipay')->success();` => 这是告诉支付宝,我们后端的处理已经完成了,你也不用再请求我们这个方法了.
+
+ * 本地开发后端回调需要处理的就是因为服务器不在互联网上,所以需要用 requestBin 接收支付宝的请求,然后我们自己复制支付宝请求的相关内容,在自己本地进行处理.
+
+# 微信支付-准备工作
+> 微信支付需要商业资质,所以暂时无法完成,仅作了解
+1. 注册账号
+ * 访问 [微信支付商户平台](https://pay.weixin.qq.com/),注册
+ * 点击上方 "产品中心"->选择页面中的 "扫码支付"-> 开通扫码支付-> 点击产品设置-> 记住商户号
+ * 然后跳转到 "账户中心"->左侧选择 "API 安全"-> 点击页面中的 "下载证书"
+ * 将下载好的文件中的 apiclient_cert.pem 以及 apiclient_key.pem 放到项目的 resources/wechat_pay/ 中(新建一个目录)
+ * 回到 "API 安全" 页, 选择页面中的 "设置密钥", 这个密钥可以在 [这里](http://www.unit-conversion.info/texttools/random-string-generator/) 生成, 页面中 length 填32,点击 "Generate" 生成一个32位的随机数,填写到微信 "设置密钥" 中去
+2. 编辑配置文件
+```
+ /**
+ * 微信支付需要的参数
+ */
+ 'wechat' => [
+ 'app_id' => 'wx*******', // 公众号 app id
+ 'mch_id' => '14*****', // 第一步获取到的商户号
+ 'key' => '******', // 刚刚设置的 API 密钥
+ 'cert_client' => resource_path('wechat_pay/apiclient_cert.pem'),
+ 'cert_key' => resource_path('wechat_pay/apiclient_key.pem'),
+ 'log' => [
+ 'file' => storage_path('logs/wechat_pay.log'),
+ ],
+ ],
+```
+
+# 微信支付
+* PaymentController => 拉起支付和支付回调
+```
+ /**
+ * 微信支付-拉起网页扫码支付
+ */
+ public function payByWechat(Order $order, Request $request)
+ {
+ // 校验权限
+ $this->authorize('own', $order);
+ // 校验订单状态
+ if ($order->paid_at || $order->closed) {
+ throw new InvalidRequestException('订单状态不正确');
+ }
+ // scan 方法为拉起微信扫码支付
+ return app('wechat_pay')->scan([
+ 'out_trade_no' => $order->no, // 商户订单流水号,与支付宝 out_trade_no 一样
+ 'total_fee' => $order->total_amount * 100, // 与支付宝不同,微信支付的金额单位是分。
+ 'body' => '支付 Laravel Shop 的订单:'.$order->no, // 订单描述
+ ]);
+ }
+
+ /**
+ * 微信支付-回调函数
+ */
+ public function wechatNotify()
+ {
+ // 校验回调参数是否正确
+ $data = app('wechat_pay')->verify();
+ // 找到对应的订单
+ $order = Order::where('no', $data->out_trade_no)->first();
+ // 订单不存在则告知微信支付
+ if (!$order) {
+ return 'fail';
+ }
+ // 订单已支付
+ if ($order->paid_at) {
+ // 告知微信支付此订单已处理
+ return app('wechat_pay')->success();
+ }
+
+ // 将订单标记为已支付
+ $order->update([
+ 'paid_at' => Carbon::now(),
+ 'payment_method' => 'wechat',
+ 'payment_no' => $data->transaction_id,
+ ]);
+
+ return app('wechat_pay')->success();
+ }
+```
+* 配置路由 routes/web.php
+```
+ Route::group(['middleware' => 'email_verified'], function () {
+
+ ...
+
+ Route::get('payment/{order}/wechat', 'PaymentController@payByWechat')->name('payment.wechat'); //微信扫码支付
+ });
+ Route::post('payment/wechat/notify', 'PaymentController@wechatNotify')->name('payment.wechat.notify'); //微信支付-后端回调
+```
+> 同理,扫码支付需要写在用户已登陆的路由组里,而后端回调不可以.
+* 在服务容器的注入中,设置配置信息 app/Providers/AppServiceProvider@register
+```
+ // 注入 wechat_pay 对象
+ $this->app->singleton('wechat_pay', function () {
+ // 获取配置文件信息
+ $config = config('pay.wechat');
+ // **配置后端回调地址**
+ $config['notify_url'] = route('payment.wechat.notify');
+ // 判断当前项目是否为测试环境
+ if (app()->environment() !== 'production') {
+ $config['log']['level'] = Logger::DEBUG;
+ // 因为微信支付没有 dev 开发模式,所以这里之配置日志即可
+ } else {
+ $config['log']['level'] = Logger::WARNING;
+ }
+ // 返回一个实例化 wechat 微信支付对象
+ return Pay::wechat($config);
+ });
+```
+* 同时在 app/Http/Middleware/VerifyCsrfToken.php 中声明后端回调地址不需要 csrf 认证
+```
+ /**
+ * 取消 csrf 认证
+ */
+ protected $except = [
+ 'payment/alipay/notify', // 支付宝
+ 'payment/wechat/notify', // 微信
+ ];
+```
+------------------------------------------------------------------------
+* 在视图层添加微信支付按钮 ../orders/show.blade.php
+```
+
+```
+--------------------------------------------------------------------------
+* 测试,点击微信支付,会返回一段 json => (这其实是微信返回的二维码),可以在 [这里](https://cli.im/) 将这个 json 中的code_url 转为二维码
+* 扫描该二维码,就可以完成支付了.
+* 如果要用 requestBin 进行后端回调的测试,那么
+ 1. 在 app/Providers/AppServiceProvider@register 中设置回调地址为 requestBin 的地址
+ 2. 扫码支付成功后, requestBin 服务器中捕获到的数据是一个 xml 文本,用 `curl -XPOST http://shop.test/payment/wechat/notify -d'XML请求包'` 来请求一次,就可以调用后端回调,更新我们的数据库
+
+# 完善微信支付:生成二维码
+1. 安装二维码生成扩展 `composer require endroid/qr-code`
+2. 改写 PaymentController@payByWechat
+```
+use Endroid\QrCode\QrCode; // <= 引用二维码扩展
+
+...
+
+ /**
+ * 微信支付-拉起网页扫码支付
+ */
+ public function payByWechat(Order $order, Request $request)
+ {
+ // 授权
+ $this->authorize('own', $order);
+
+ // 确保订单状态
+ if ($order->paid_at || $order->closed) {
+ throw new InvalidRequestException('订单状态不正确');
+ }
+
+ // 实例化支付对象(但不直接 return 而是保存在变量中)
+ $wechatOrder = app('wechat_pay')->scan([
+ 'out_trade_no' => $order->no,
+ 'total_fee' => $order->total_amount * 100,
+ 'body' => '支付 Laravel Shop 的订单:'.$order->no,
+ ]);
+
+ // 生成二维码
+ $qrCode = new QrCode($wechatOrder->code_url);
+
+ // 将生成的二维码图片数据以字符串形式输出,并带上相应的响应类型
+ return response($qrCode->writeString(), 200, ['Content-Type' => $qrCode->getContentType()]);
+ }
+```
+* 编辑 ../orders/show.blade.php
+```
+{{-- 按钮换一下 --}}
+
+
+{{-- 前端代码:异步请求生成二维码让用户支付 --}}
+@section('scriptsAfterJs')
+
+@endsection
+```
+
+# 支付后的逻辑
+> 支付之后要给订单中的商品增加销量,比如我们要发邮件给用户告知订单支付成功。
+
+> 商品增加销量和发送邮件并不会影响到订单的支付状态,即使这两个操作失败了也不影响我们后续的业务流程,对于此类需求我们通常使用异步事件来解决。
+
+1. 创建支付成功事件 `php artisan make:event OrderPaid` => 位于 app/Events/
+```
+order = $order;
+ }
+
+ public function getOrder()
+ {
+ return $this->order;
+ }
+}
+```
+> 事件本身不需要有逻辑,只需要包含相关的信息即可,在我们这个场景里就只需要一个订单对象。
+2. 编辑 PaymentController : 增加一个事件触发方法, 编辑支付宝和微信支付支付成功后的后台回调,在返回前触发事件
+```
+ /**
+ * 支付完成后异步触发事件更新商品被购买次数
+ */
+ protected function afterPaid(Order $order)
+ {
+ event(new OrderPaid($order)); // 触发一个新的事件
+ }
+
+ /**
+ * 微信支付-回调函数
+ */
+ public function wechatNotify()
+ {
+ ...
+
+
+ // 触发事件
+ $this->afterPaid($order);
+
+ return app('wechat_pay')->success();
+ }
+
+ // 支付宝同理
+```
+> `event(new 事件类(参数))` => 触发事件
+3. 创建事件监听器,处理逻辑 `php artisan make:listener UpdateProductSoldCount --event=OrderPaid` => 位于 app/Listeners
+```
+getOrder();
+ // 循环遍历订单的商品
+ foreach ($order->items as $item) {
+ $product = $item->product;
+ // 计算对应商品的销量
+ $soldCount = OrderItem::query()
+ ->where('product_id', $product->id)
+ ->whereHas('order', function ($query) {
+ $query->whereNotNull('paid_at'); // 关联的订单状态是已支付
+ })->sum('amount');
+ // 更新商品销量
+ $product->update([
+ 'sold_count' => $soldCount,
+ ]);
+ }
+ }
+}
+```
+4. 关联事件和监听器 app/Providers/EventServiceProvider.php
+```
+ /**
+ * 关联事件和监听器
+ */
+ protected $listen = [
+ 'App\Events\Event' => [
+ 'App\Listeners\EventListener',
+ ],
+ Registered::class => [
+ RegisteredListener::class,
+ ],
+ // **添加这里**
+ \App\Events\OrderPaid::class => [
+ \App\Listeners\UpdateProductSoldCount::class,
+ ],
+ ];
+```
+5. 捋一下逻辑:控制器写一个方法 `new 一个事件类` => 但是事件类本身没有动作 => 所以需要事件监听器来完成触发事件之后的动作 => 同时要记得绑定事件类和事件监听器.(为什么不直接写逻辑代码,而是调事件的原因接下来说)
+----------------------------------------------------------------
+1. 创建支付成功后的邮件通知 `php artisan make:notification OrderPaidNotification`
+```
+order = $order;
+ }
+
+ // 我们只需要通过邮件通知,因此这里只需要一个 mail 即可
+ public function via($notifiable)
+ {
+ return ['mail'];
+ }
+
+ public function toMail($notifiable)
+ {
+ return (new MailMessage)
+ ->subject('订单支付成功') // 邮件标题
+ ->greeting($this->order->user->name.'您好:') // 欢迎词
+ ->line('您于 '.$this->order->created_at->format('m-d H:i').' 创建的订单已经支付成功。') // 邮件内容
+ ->action('查看订单', route('orders.show', [$this->order->id])) // 邮件中的按钮及对应链接
+ ->success(); // 按钮的色调
+ }
+}
+```
+2. 创建监听器 `php artisan make:listener SendOrderPaidMail --event=OrderPaid`
+```
+getOrder();
+ // 调用 notify 方法来发送通知
+ $order->user->notify(new OrderPaidNotification($order));
+ }
+}
+```
+3. 关联监听器和事件 EventServiceProvider.php
+```
+ protected $listen = [
+ 'App\Events\Event' => [
+ 'App\Listeners\EventListener',
+ ],
+ Registered::class => [
+ RegisteredListener::class,
+ ],
+ \App\Events\OrderPaid::class => [
+ \App\Listeners\UpdateProductSoldCount::class,
+ \App\Listeners\SendOrderPaidMail::class, // <= 这里
+ ],
+ ];
+```
+4. 事件没有逻辑代码,而是触发事件时用监听器的原因:
+ * 一是因为一个事件可能触发多个动作,所以写多个监听器就好(这里触发支付完订单之后的动作,需要增加销量,同时发送邮件通知用户)
+ * 二是因为监听器里面可以异步执行
+---------------------------------------------------------------------
+5. 测试
+ 1. 再次完成一次购买(记得后台接口请求一次)
+ 2. 输入命令 `php artisan queue:work` 开始执行队列中的任务
\ No newline at end of file
diff --git a/laravel/laravel5/9.md b/laravel/laravel5/9.md
new file mode 100644
index 0000000..bb4cd0b
--- /dev/null
+++ b/laravel/laravel5/9.md
@@ -0,0 +1,472 @@
+# 后台-订单列表
+* 创建控制器 `php artisan admin:make OrdersController --model=App\\Models\\Order`
+```
+header('订单列表');
+ $content->body($this->grid());
+ });
+ }
+
+ /**
+ * 订单表格
+ */
+ protected function grid()
+ {
+ return Admin::grid(Order::class, function (Grid $grid) {
+ // 只展示已支付的订单,并且默认按支付时间倒序排序
+ $grid->model()->whereNotNull('paid_at')->orderBy('paid_at', 'desc');
+
+ $grid->no('订单流水号');
+ // 展示关联关系的字段时,使用 column 方法
+ $grid->column('user.name', '买家');
+ $grid->total_amount('总金额')->sortable();
+ $grid->paid_at('支付时间')->sortable();
+ $grid->ship_status('物流')->display(function($value) {
+ return Order::$shipStatusMap[$value];
+ });
+ $grid->refund_status('退款状态')->display(function($value) {
+ return Order::$refundStatusMap[$value];
+ });
+ // 禁用创建按钮,后台不需要创建订单
+ $grid->disableCreateButton();
+ $grid->actions(function ($actions) {
+ // 禁用删除和编辑按钮
+ $actions->disableDelete();
+ $actions->disableEdit();
+ });
+ $grid->tools(function ($tools) {
+ // 禁用批量删除按钮
+ $tools->batch(function ($batch) {
+ $batch->disableDelete();
+ });
+ });
+ });
+ }
+}
+```
+* 配置路由 app/Admin/routes.php
+```
+$router->get('orders', 'OrdersController@index')->name('admin.orders.index'); //订单列表
+```
+
+# 后台-订单详情
+* app/Admin/Controllers/OrdersController@show
+```
+ /**
+ * 订单详情
+ */
+ public function show(Order $order)
+ {
+ return Admin::content(function (Content $content) use ($order) {
+ $content->header('查看订单');
+ // body 方法可以接受 Laravel 的视图作为参数
+ $content->body(view('admin.orders.show', ['order' => $order]));
+ });
+ }
+```
+> 这里用 `$content-body(view())` => 指定后台视图
+* 配置路由
+```
+$router->get('orders/{order}', 'OrdersController@show')->name('admin.orders.show'); //订单详情
+```
+* 视图 (../admin/orders/show.blade.php )详情参考教程,略。
+
+# 后台-订单发货
+* OrdersController@ship
+```
+use Illuminate\Http\Request;
+use App\Exceptions\InvalidRequestException;
+
+...
+
+ /**
+ * 订单发货
+ */
+ public function ship(Order $order, Request $request)
+ {
+ // 判断当前订单是否已支付
+ if (!$order->paid_at) {
+ throw new InvalidRequestException('该订单未付款');
+ }
+
+ // 判断当前订单发货状态是否为未发货
+ if ($order->ship_status !== Order::SHIP_STATUS_PENDING) {
+ throw new InvalidRequestException('该订单已发货');
+ }
+
+ // Laravel 5.5 之后 validate 方法可以返回校验过的值
+ $data = $this->validate($request, [
+ 'express_company' => ['required'],
+ 'express_no' => ['required'],
+ ], [], [
+ 'express_company' => '物流公司',
+ 'express_no' => '物流单号',
+ ]);
+
+ // 将订单发货状态改为已发货,并存入物流信息
+ $order->update([
+ 'ship_status' => Order::SHIP_STATUS_DELIVERED,
+ // 我们在 Order 模型的 $casts 属性里指明了 ship_data 是一个数组
+ // 因此这里可以直接把数组传过去
+ 'ship_data' => $data,
+ ]);
+
+ // 返回上一页
+ return redirect()->back();
+ }
+```
+* 配置路由
+```
+$router->post('orders/{order}/ship', 'OrdersController@ship')->name('admin.orders.ship'); //订单发货
+```
+* 后台订单详情页做一个判断:如果商品物流状态处于未发货的状态,则增加一个发货表单(要求填写物流公司和物流单号),否则就显示物流公司和物流单号。
+
+# 前台-确认收货
+* app/Http/OrdersController@received
+```
+ /**
+ * 确认收货
+ */
+ public function received(Order $order, Request $request)
+ {
+ // 校验权限
+ $this->authorize('own', $order);
+
+ // 判断订单的发货状态是否为已发货
+ if ($order->ship_status !== Order::SHIP_STATUS_DELIVERED) {
+ throw new InvalidRequestException('发货状态不正确');
+ }
+
+ // 更新发货状态为已收到
+ $order->update(['ship_status' => Order::SHIP_STATUS_RECEIVED]);
+
+ // 返回订单信息
+ return $order;
+ }
+```
+* 配置路由 routes/web.php
+```
+Route::post('orders/{order}/received', 'OrdersController@received')->name('orders.received'); //确认收货
+```
+* 订单详情视图 (../orders/show.blade.php) 在订单编号下面增加当前物流状态,同时在支付按钮那一部分作一个判断:如果已支付且已发货则显示确认收获按钮
+```
+{{-- 确认收货 --}}
+@if($order->ship_status === \App\Models\Order::SHIP_STATUS_DELIVERED)
+
+@endif
+
+...
+
+ // 确认收货按钮
+ $('#btn-receive').click(function() {
+ // 弹出确认框
+ swal({
+ title: "确认已经收到商品?",
+ icon: "warning",
+ buttons: true,
+ dangerMode: true,
+ buttons: ['取消', '确认收到'],
+ })
+ .then(function(ret) {
+ // 如果点击取消按钮则不做任何操作
+ if (!ret) {
+ return;
+ }
+ // ajax 提交确认操作
+ axios.post('{{ route('orders.received', [$order->id]) }}')
+ .then(function () {
+ // 刷新页面
+ location.reload();
+ })
+ });
+ });
+```
+
+# 用户-商品评价
+* 创建一个 Request => 用于验证评价内容 `php artisan make:request SendReviewRequest`
+```
+ ['required', 'array'],
+ 'reviews.*.id' => [
+ 'required',
+ Rule::exists('order_items', 'id')->where('order_id', $this->route('order')->id)
+ ],
+ 'reviews.*.rating' => ['required', 'integer', 'between:1,5'],
+ 'reviews.*.review' => ['required'],
+ ];
+ }
+
+ /**
+ * 字段名称
+ */
+ public function attributes()
+ {
+ return [
+ 'reviews.*.rating' => '评分',
+ 'reviews.*.review' => '评价',
+ ];
+ }
+}
+```
+> `Rule::exists()` => 判断用户提交的 ID 是否属于此订单
+
+> `$this->route('order')` => 获得当前路由对应的订单对象
+
+* 由于评价系统基于订单,所以将评价页面 (review) 和提交评价方法 (sendReview) 都写在 OrdersController 中
+```
+use Carbon\Carbon;
+use App\Http\Requests\SendReviewRequest;
+
+...
+
+ /**
+ * 评价页面
+ */
+ public function review(Order $order)
+ {
+ // 校验权限
+ $this->authorize('own', $order);
+ // 判断是否已经支付
+ if (!$order->paid_at) {
+ throw new InvalidRequestException('该订单未支付,不可评价');
+ }
+ // 使用 load 方法加载关联数据,避免 N + 1 性能问题
+ return view('orders.review', ['order' => $order->load(['items.productSku', 'items.product'])]);
+ }
+
+ /**
+ * 保存评论
+ */
+ public function sendReview(Order $order, SendReviewRequest $request)
+ {
+ // 校验权限
+ $this->authorize('own', $order);
+ if (!$order->paid_at) {
+ throw new InvalidRequestException('该订单未支付,不可评价');
+ }
+
+ // 判断是否已经评价
+ if ($order->reviewed) {
+ throw new InvalidRequestException('该订单已评价,不可重复提交');
+ }
+ $reviews = $request->input('reviews');
+
+ // 开启事务
+ \DB::transaction(function () use ($reviews, $order) {
+ // 遍历用户提交的数据
+ foreach ($reviews as $review) {
+ $orderItem = $order->items()->find($review['id']);
+ // 保存评分和评价
+ $orderItem->update([
+ 'rating' => $review['rating'],
+ 'review' => $review['review'],
+ 'reviewed_at' => Carbon::now(),
+ ]);
+ }
+ // 将订单标记为已评价
+ $order->update(['reviewed' => true]);
+ });
+
+ return redirect()->back();
+ }
+```
+* 配置路由 routes/web.php
+```
+Route::get('orders/{order}/review', 'OrdersController@review')->name('orders.review.show'); //商品评价页
+Route::post('orders/{order}/review', 'OrdersController@sendReview')->name('orders.review.store'); //保存评价
+```
+* 评价页面视图 (../orders/review.blade.php) 详情和样式参考教程,有个打分星星值得学习。
+* 增加入口 ../orders/index.blade.php , show.blade.php 同理(只是还需要判断是否确认收货)
+```
+@if($order->paid_at)
+
+ {{ $order->reviewed ? '查看评价' : '评价' }}
+
+@endif
+```
+* 更新商品评分
+ 1. 创建事件 `php artisan make:event OrderReviewd` => 事件没有逻辑,而是当触发事件时,执行的是监听器中的代码,因此我们在事件中只需要确定并且获取操作需要的数据即可(这里我们需要订单的详细信息,即订单对象)
+ ```
+ order = $order;
+ }
+
+ public function getOrder()
+ {
+ return $this->order;
+ }
+ }
+ ```
+ 2. 创建监听器 `php artisan make:listener UpdateProductRating --event=OrderReviewd`
+ ```
+ getOrder()->items()->with(['product'])->get();
+ foreach ($items as $item) {
+ $result = OrderItem::query()
+ ->where('product_id', $item->product_id)
+ ->whereHas('order', function ($query) {
+ $query->whereNotNull('paid_at');
+ })
+ ->first([
+ DB::raw('count(*) as review_count'),
+ DB::raw('avg(rating) as rating')
+ ]);
+ // 更新商品的评分和评价数
+ $item->product->update([
+ 'rating' => $result->rating,
+ 'review_count' => $result->review_count,
+ ]);
+ }
+ }
+ }
+ ```
+ 3. 在 app/Providers/EventServiceProvider.php 中关联事件和监听器
+ ```
+ protected $listen = [
+
+ ...
+
+ \App\Events\OrderReviewd::class => [
+ \App\Listeners\UpdateProductRating::class,
+ ],
+ ];
+ ```
+ 4. 在控制器层触发事件
+ ```
+ use App\Events\OrderReviewd;
+
+ ...
+
+ /**
+ * 保存评论
+ */
+ public function sendReview(Order $order, SendReviewRequest $request)
+ {
+ ...
+
+ // 开启事务
+ \DB::transaction(function () use ($reviews, $order) {
+
+ ...
+
+ // 触发事件,更新评分
+ event(new OrderReviewd($order));
+ });
+
+ return redirect()->back();
+ }
+ ```
+
+# 前台-展示商品评价和评分
+* 完善 ProdocutsController@show
+```
+use App\Models\OrderItem;
+
+...
+
+ /**
+ * 商品详情
+ */
+ public function show(Product $product, Request $request)
+ {
+ // 判断商品是否已经上架,如果没有上架则抛出异常。
+ if (!$product->on_sale) {
+ throw new InvalidRequestException('商品未上架');
+ }
+
+ // 默认当前商品没有被喜欢(没登陆的用户也需要看到的是 “收藏” 按钮)
+ $favored = false;
+
+ // 判断一下当前用户是否登陆,如果已登陆,那么判断一下是否喜欢该商品
+ if($user = $request->user()) {
+ $favored = boolval($user->favoriteProducts()->find($product->id)); // boolval() => 将参数转为布尔类型
+ }
+
+ // 获取评价
+ $reviews = OrderItem::query()
+ ->with(['order.user', 'productSku']) // 预先加载关联关系
+ ->where('product_id', $product->id)
+ ->whereNotNull('reviewed_at') // 筛选出已评价的
+ ->orderBy('reviewed_at', 'desc') // 按评价时间倒序
+ ->limit(10) // 取出 10 条
+ ->get();
+
+ // 跳转到视图
+ return view('products.show', [
+ 'product' => $product,
+ 'favored' => $favored,
+ 'reviews' => $reviews
+ ]);
+ }
+```
\ No newline at end of file
diff --git "a/laravel/\346\257\225\344\270\232\350\256\276\350\256\241-\346\224\271/1.md" "b/laravel/\346\257\225\344\270\232\350\256\276\350\256\241-\346\224\271/1.md"
new file mode 100644
index 0000000..b2f6978
--- /dev/null
+++ "b/laravel/\346\257\225\344\270\232\350\256\276\350\256\241-\346\224\271/1.md"
@@ -0,0 +1,196 @@
+# 思路整理-用户
+1. 后台管理员
+ * 可以管理用户
+ * 可以管理门店
+ * 可以管理评论
+ * 可以下架已上架的商品
+ * 可以管理订单
+ > 后台通过 laravel-admin 实现权限分配和管理
+2. 商户管理员
+ * 可以管理商品
+ * 可以管理订单
+3. 普通用户
+ * 游客用户可以查看门店,商品详情等
+ * 而登陆的用户可以操作购物车,并且创建订单
+ > 商户管理员和普通用户用 laravel-permission 分级管理
+
+# 创建项目
+1. `code ~/Homestead/Homestead.yaml` => 增加 homestead 中的站点和数据库
+```
+# 站点配置
+sites:
+ # gydiner
+ - map: gydiner.test
+ to: /home/vagrant/Code/gydiner/public
+
+# 数据库名称
+databases:
+ # gydiner
+ - gydiner
+```
+2. `notepad C:/Windows/System32/Drivers/etc/hosts` => 增加 hosts , 以便在 windows 中访问项目(需要以管理员身份执行命令)
+```
+192.168.10.10 gydiner.test
+```
+3. `cd ~/Homestead && vagrant up` 开启 homestead , `vagrant ssh` 登陆 homestead , `cd ~/Code` 切换到存放代码的目录, `composer create-project laravel/laravel gydiner --prefer-dist "5.5.*"` => 创建名为 gydiner (广元食客) 的项目。
+4. `cd ~/Homestead && vagrant provision` => 让 homestead 读取最新的配置文件信息, 此时会读取 Homestead.yaml 配置信息,创建新的虚拟站点和数据库。
+5. 访问 http://gydiner.test/ 即可看到部署好的 laravel 框架,用编辑器打开项目,作一些配置
+ * 编辑 .env
+ ```
+ APP_NAME=gydiner
+ ...
+ APP_URL=http://gydiner.test/
+ ...
+ DB_DATABASE=gydiner
+ ```
+ * 编辑 config/app.php => 设置时区和语言
+ ```
+ 'timezone' => 'Asia/Shanghai',
+ 'locale' => 'zh-CN',
+ ```
+
+# 全局辅助函数(助手函数)
+1. 创建 bootstrap/helpers.php
+ ```
+ name('root'); //首页
+```
+3. 视图:创建 ../pages/root.blade.php => 继承 app.blade.php, 填充 content。
+
+# Yarn 与前端扩展(可选)
+> 所有样式都写在 resources/assets/sass/app.scss 中,但是现在还没装 npm 包,不能用 laravel-mix 编译写好的 app.scss 样式, 如果不用 Yarn, 可以直接配置 cnpm, 然后执行 `cnpm install`
+1. `yarn config set registry https://registry.npm.taobao.org` 设置 Yarn 下载地址为淘宝镜像
+2. 装扩展 `yarn install --no-bin-links`
+3. 修改 package.json
+```
+"scripts":{
+ "dev": "npm run development",
+ "development": "NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
+ "watch": "NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
+ "watch-poll": "npm run watch -- --watch-poll",
+ "hot": "NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
+ "prod": "npm run production",
+ "production": "NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
+},
+```
+> 在 windows.homestead 中用 Yarn 都得加参数 --no-bin-links
+
+# 用户认证脚手架
+* 创建一个 Seeder `php artisan make:seeder UsersTableSeeder`
+```
+times(10)->create();
+
+ // 找到第1条数据并配置
+ $user = User::find(1);
+ $user->name = '示例用户';
+ $user->email = 'woshimiexiaoming@foxmail.com';
+ $user->password = bcrypt('ceshi');
+ $user->save();
+ }
+}
+```
+> 模型工厂自带,所以不用做,写完了之后需要在 DatabaseSeeder@run 中 `$this->call(UsersTableSeeder::class);`,完成之后执行 `php artisan migrate --seed`
+
+* 执行命令 `php artisan make:auth` 生成 Laravel 自带的用户认证脚手架,会问是否覆盖布局视图,因为已经写好了所以选 No .
+> 汉化一下视图,就完成了用户功能(登陆、注册)
+
+* 编辑路由文件 routes/web.php,不需要 `'/home'` 路由,同时全局查找替换用户认证功能相关控制器的跳转地址为 `'/'`
+
+* 编辑 ../layouts/_header.blade.php 视图,给登陆和注册按钮绑定正确的路由地址。
+
+* 安装中文包 `composer require "overtrue/laravel-lang"`
+
+* 配置163邮件提供的免费 smtp 服务 (.env)
+```
+MAIL_DRIVER=smtp
+MAIL_HOST=smtp.163.com
+MAIL_PORT=25
+MAIL_USERNAME=邮箱地址
+MAIL_PASSWORD=smtp服务密码
+MAIL_ENCRYPTION=加密(默认写 null)
+MAIL_FROM_ADDRESS=发件人邮箱
+MAIL_FROM_NAME=发件人地址
+```
+
+# 提交代码到 GitHub
+1. `git init` => 初始化本地 git
+2. `git add .`, `git commit -m "git commit -m "创建项目,布局模板,首页,前端扩展,用户认证脚手架"` => 作一次本地提交
+3. `git remote add origin git@github.com:prohorry-me/gydiner.git` => 配置远程git地址(需要[新建 Git 仓库](https://github.com/new))
+4. `git push` => 推送代码到远程仓库
\ No newline at end of file
diff --git "a/laravel/\346\257\225\344\270\232\350\256\276\350\256\241-\346\224\271/10.md" "b/laravel/\346\257\225\344\270\232\350\256\276\350\256\241-\346\224\271/10.md"
new file mode 100644
index 0000000..e6a7041
--- /dev/null
+++ "b/laravel/\346\257\225\344\270\232\350\256\276\350\256\241-\346\224\271/10.md"
@@ -0,0 +1,274 @@
+# Cache 实现购物车
+1. 创建控制器 `php artisan make:controller CartController`, 内容如下
+```
+user()->id;
+
+ // 获取购物车信息
+ $cart = Cache::get($cartKey);
+
+ // 如果购物车信息为空
+ if(!$cart) {
+ $cart = $this->initializeCart($product); //初始化购物车
+ $cart['total_price'] = $this->getTotalPrice($cart); //计算总价
+ Cache::set($cartKey, $cart, 360); //保存信息6小时。
+
+ session()->flash('success', '您正在【' . $product->shop->name . '】中购买食品, 已添加【' . $product->name . '】到购物车');
+ return redirect()->back();
+ }
+
+ // 如果购物车不为空,那么首先判断是否在本店购物(因为是美食 o2o ,几乎没有人同时在两家店点餐,所以一家店一个购物车)
+ if($cart['shop_id'] != $product->shop->id) {
+ $cart = $this->initializeCart($product);
+ $cart['total_price'] = $this->getTotalPrice($cart);
+ Cache::set($cartKey, $cart, 360);
+
+ session()->flash('success', '您已换到【' . $product->shop->name . '】中购买食品, 已添加【' . $product->name . '】到购物车');
+ return redirect()->back();
+
+ // 如果是在本店购物
+ }else {
+ // 遍历购物车找商品:如果找到,增加已在购物车中的商品的数量
+ foreach($cart['data'] as $index => $data) {
+ if($data['product_id'] == $product->id) {
+ $cart['data'][$index]['product_count'] += 1;
+ $cart['total_price'] = $this->getTotalPrice($cart);
+ Cache::set($cartKey, $cart, 360);
+
+ session()->flash('success', '增加了1个【' . $product->name . '】');
+ return redirect()->back();
+ }
+ }
+
+ // 没找到,则说明添加的商品在购物车中不存在
+ $cart['data'][] = [
+ 'product_image' => $product->image,
+ 'product_id' => $product->id,
+ 'product_name' => $product->name,
+ 'product_price' => $product->price,
+ 'product_count' => 1,
+ ];
+
+ $cart['total_price'] = $this->getTotalPrice($cart);
+ Cache::set($cartKey, $cart, 360);
+
+ session()->flash('success', '已添加【' . $product->name . '】到购物车');
+ return redirect()->back();
+ }
+ }
+
+ /**
+ * 初始化购物车
+ */
+ public function initializeCart($product)
+ {
+ $cart = [
+ 'shop_id' => $product->shop->id,
+ 'shop_name' => $product->shop->name,
+ 'data' => [
+ [
+ 'product_image' => $product->image,
+ 'product_id' => $product->id,
+ 'product_name' => $product->name,
+ 'product_price' => $product->price,
+ 'product_count' => 1, //默认一个商品
+ ],
+ ],
+ ];
+
+ return $cart;
+ }
+
+ /**
+ * 计算总价
+ */
+ public function getTotalPrice($cart)
+ {
+ $totalPrice = 0;
+ foreach($cart['data'] as $index => $data) {
+ // 计算总价
+ $totalPrice += $data['product_count'] * $data['product_price'];
+ }
+
+ return $totalPrice;
+ }
+
+ /**
+ * 购物车详情页
+ */
+ public function show(Request $request)
+ {
+ // 配置购物车名称
+ $cartKey = 'cart' . $request->user()->id;
+
+ // 获取购物车信息
+ $cart = Cache::get($cartKey);
+
+ return view('cart.show', [
+ 'cart' => $cart,
+ ]);
+ }
+
+ /**
+ * 减少(删除) 购物车中的商品
+ */
+ public function reduce(Request $request, $product)
+ {
+ // 配置购物车名称
+ $cartKey = 'cart' . $request->user()->id;
+
+ // 获取购物车信息
+ $cart = Cache::get($cartKey);
+
+ foreach($cart['data'] as $index => $data) {
+ if($data['product_id'] == $product) {
+ $cart['data'][$index]['product_count'] -= 1;
+
+ // 当商品数量小于0时,从数组中剔除商品
+ if($cart['data'][$index]['product_count'] <= 0) {
+ unset($cart['data'][$index]);
+ }
+
+ $cart['total_price'] = $this->getTotalPrice($cart);
+ Cache::set($cartKey, $cart, 360);
+
+ session()->flash('success', '减少商品成功');
+ return redirect()->back();
+ }
+ }
+ }
+}
+```
+> Cache 是存储在服务器上的缓存信息,可以通过 `Cache::set($key, $value, 时间单位分钟)` 来保存信息。$value 可以是任意类型的值。同时也可以通过 `Cache::get($key)` 来获取信息。 我们的购物车标识则为 `'cart'` 加上 `用户id`
+
+> 添加逻辑较为复杂:首先尝试读取 Cache 中的购物车信息。如果没有,则初始化购物车,然后计算总价,最后保存在 Cache 中。且项目中,一个商店对应一个购物车,不支持跨店购买。
+
+> 减少(删除)逻辑则需要注意,不能让商品个数为0和负数,所以当数量小于等于0时,剔除数组。
+
+2. 配置路由
+```
+Route::get('/cart/show', 'CartController@show')->name('cart.show'); //购物车页面
+Route::get('/cart/add/{product}', 'CartController@add')->name('cart.add'); //往购物车里面添加商品或者增加数量
+Route::get('/cart/reduce/{product}', 'CartController@reduce')->name('cart.reduce'); //从购物车中减少某商品
+```
+
+3. CartController@show 方法对应的视图就是一个表格,显示购物车信息。
+
+4. 在 app/Http/ShopsController@show => 前台控制器上,则需要
+```
+use Cache;
+
+ /**
+ * 门店详情
+ */
+ public function show(Request $request, Shop $shop)
+ {
+ // 默认当前商店没有被喜欢(没登陆的用户也需要看到的是 “收藏” 按钮)
+ $favored = false;
+ $cart = []; // <=购物车默认为空
+
+ // 判断一下当前用户是否登陆,如果已登陆,那么判断一下是否喜欢该商店
+ if($user = $request->user()) {
+ $favored = boolval($user->favoriteShops()->find($shop->id)); // boolval() => 将参数转为布尔类型
+
+ // 获取购物车信息
+ $cartKey = 'cart' . $user->id;
+ $cart = Cache::get($cartKey);
+ }
+
+ return view('shops.show', [
+ 'shop' => $shop,
+ 'favored' => $favored,
+ 'cart' => $cart
+ ]);
+ }
+```
+> 如果用户登陆则再获取购物车信息,交给视图
+
+* 视图 ../shops/show.blade.php 通过判断 $cart 的值,更改购物车按钮的样式,显示购物车金额(空 or 购物车金额)
+
+# 购物车页面增加一个表单用于提交订单
+* ../cart/show.blade.php
+```
+
+```
+> 没写 action 是先把表单写在这,这个表单用于之后生成订单,先完成其他功能。
+
+# 选择/生成收货地址
+* 上面的表单有一个按钮
+```
+
+```
+* 这个按钮触发一个模态框,里面有一个表单用于添加收货地址 `