用户模块开发
解决因为watch-poll导致CPU负载过高
"watch-poll": "npm run watch -- --watch-poll=5000",
建议在本机跑watch-poll
,不要在vagrant
容器中跑
快速将数据库字段值转换成常见的数据类型
// App\Models\User
protected $casts = ['email_verified' => 'boolean']
$casts
属性提供了一个便利的方法来将数据库字段值转换为常见的数据类型,$casts
属性应是一个数组,且数组的键是那些需要被转换的字段名,值则是你希望转换的数据类型。支持转换的数据类型有:integer
,real
,float
,double
,string
,boolean
,object
,array
,collection
,date
,datetime
和timestamp
。
Laravel中如何判断Request是AJAX
/**
* Determine if the current request probably expects a JSON response.
*/
if($request->expectsJson()) {
// ...
}
用户验证邮箱的步骤
原理:在通知发送前,将用户的email
作为key
,将由随机字符串组成的token
作为value
,并将这对键值对存入Cache
中;用户收到邮件后,点击“验证邮箱”链接,通过参数中的email
取出Cache
中的token
,并与穿过来的参数中的token
进行比对。
-
先定义消息通知类
EmailVerificationNotification
class EmailVerificationNotification extends Notification implements ShouldQueue { use Queueable; ... public function via($notifiable) { return ['mail']; } public function toMail($notifiable) { $token = Str::random('16'); // Put key to Cache, and set the number of minutes Cache::add('email_verification_' . $notifiable->email, $token, 30); $url = route('email_verification.verify', ['email' => $notifiable->email, 'token' => $token]); return (new MailMessage) ->greeting('尊敬的 '.$notifiable->name .' '. '您好:') ->subject('注册成功,请验证您的邮箱') ->line('请点击下方链接验证您的邮箱') ->action('验证邮箱', $url); } }
-
在
EmailVerificationController
中定义验证邮箱链接的逻辑与手动发送验证邮件的逻辑class EmailVerificationController extends Controller { ... public function verify(Request $request) { $email = $request->input('email'); $token = $request->input('token'); $cache_key = 'email_verification_' . $email; // Verification URL is error if (!$email || !$token) { throw new \Exception('验证链接错误'); } // Token is error or out-of-date if ($token != Cache::get($cache_key)) { throw new \Exception('验证链接不正确或者已过期'); } // Can't find this user if (!$user = User::where('email', $email)->first()) { throw new \Exception('该用户不存在,验证失败'); } // Remove Items from the cache Cache::forget($cache_key); $user->update(['email_verified' => true]); return view('pages.success', ['msg' => '恭喜您验证邮箱成功']); } public function send(Request $request) { $user = $request->user(); if($user->email_verified) { throw new \Exception('你已经验证过邮箱了'); } // Send notification manually $user->notify(new EmailVerificationNotification()); return view('pages.success', ['msg' => '邮件发送成功']); } }
-
设置监听器,在注册事件发生后,执行发送邮件
class RegisteredListener implements ShouldQueue { ... public function handle(Registered $event) { // Can not use $event->user() $user = $event->user; $user->notify(new EmailVerificationNotification()); } }
-
在
App\Providers\EventServiceProvider
中将前面定义监听器与Auth
中的注册成功事件绑定use Illuminate\Auth\Events\Registered; ... class EventServiceProvider extends ServiceProvider { protected $listen = [ ... Registered::class => [ RegisteredListener::class, ] ]; ... }
重命名Factory文件
重命名工厂文件之后需要执行
composer dumpautoload
,否则会找不到对应的工厂文件。
如何自定义框架抛出异常的处理方式
// App\Exceptions\Handler.php
public function render($request, Exception $exception)
{
// If throw Illuminate\Auth\Access\AuthorizationException, redirect to 'root'
if ($exception instanceof AuthorizationException) {
return redirect()->route('root');
}
return parent::render($request, $exception);
}
商品模块开发
商品SKU的概念
SKU = Stock Keeping Unit(库存量单位),也可以称为『单品』。对一种商品而言,当其品牌、型号、配置、等级、花色、包装容量、单位、生产日期、保质期、用途、价格、产地等属性中任一属性与其他商品存在不同时,可称为一个单品。
多维SKU的数据库设计
首先是product
表:
字段名称 | 描述 | 字段类型 | 索引 |
---|---|---|---|
id | 自增长ID | unsigned int | 主键 |
title | 商品名称 | varchar | / |
description | 商品详情 | text | / |
image | 商品封面图片文件路径 | varchar | / |
on_sale | 商品是否上架 | boolean | / |
rating | 商品平均评分 | float, default 5 | / |
sold_count | 销量 | unsigned int, default 0 | / |
review_count | 评论数量 | unsigned int, default 0 | / |
price | SKU最低价格 | decimal(10, 2) | / |
这里注意,价格必须用decimal为单位保证精度,总精度为10,小数位精度为2
再来设计product_skus
表
字段名称 | 描述 | 字段类型stit | 索引 |
---|---|---|---|
id | 自增ID | unsigned int | 主键 |
title | SKU名称 | varchar | / |
description | SKU描述 | varchar | / |
price | SKU价格 | decimal(10, 2) | / |
stock | 库存 | unsigned int | / |
attributes | 商品属性(多维),与product_attributes_val 表对应 |
varchar | / |
product_id | 所属商品ID | unsigned int | 外键product 表的id |
表product_sku_attributes
字段名称 | 描述 | 字段类型 | 索引 |
---|---|---|---|
id | 自增ID | unsigned int | 主键 |
product_id | 所属的商品ID | unsigned int | 外键product 表的id |
name | 属性名字 | varchar | / |
表product_attr_value
字段名称 | 描述 | 字段类型 | 索引 |
---|---|---|---|
symbol | 该属性的唯一序号标记 | unsigned int | 设置为主键 |
product_id | 所属商品ID | unsigned int | 外键product 表的id ,和attr_value 是联合唯一索引 |
value | 属性值 | varchar | 和product_id 是联合唯一索引 |
attr_id | 所属的属性ID | unsigned int | 外键product_sku_attributes 表的id |
Model::query()和Model::all()的区别
Model::all()
返回的是Collection
对象
Model::query()
返回的是Query Builder
对象,需要调用get()
方法才能转换成Collection
,不过两者执行的SQL语句是不一样的,下面这句话,对应执行的SQL语句为select ... from ... where ...
, 而Model::all()
方法执行的仅仅是是select ... from
,它的where
筛选是在集合类中进行的操作而不是进行SQL筛选。
Model::query()->where(...)
与Model::where(...)
写法一致
搜索和排序都放在一个form中
<form action="{{ route('products.index') }}" class="form-inline search-form">
<input type="text" class="form-control input-sm" name="search" placeholder="搜索">
<button class="btn btn-primary btn-sm">搜索</button>
<select name="order" class="form-control input-sm pull-right">
<option value="">排序方式</option>
<option value="price_asc">价格从低到高</option>
<option value="price_desc">价格从高到低</option>
<option value="sold_count_desc">销量从高到低</option>
<option value="sold_count_asc">销量从低到高</option>
<option value="rating_desc">评价从高到低</option>
<option value="rating_asc">评价从低到高</option>
</select>
</form>
这样做的好处是修改了order,搜索的参数依旧不会变,还是这个搜索结果下的排序。
对于orWhereHas
的理解
示例代码如下,此处的orWhereHas
实际上是增加自定义条件到相应关联的约束中,因为此处product
表和sku
表是一对多的关系。
$builder = Product::where('on_sale', true);
// Deal with Search Request
if ($search = $request->input('search', '')) {
$like = '%' . $search . '%';
$builder->where(function ($query) use ($like) {
$query->where('title', 'like', $like)
->orWhere('description', 'like', $like)
->orWhereHas('skus', function ($query) use ($like) {
$query->where('title', 'like', $like)
->orWhere('description', 'like', $like);
});
});
}
可以看到执行的SQL语句如下,关联部分就是or exists
后面的部分:
select * from products
where on_sale
= 1 and (title
like '%nisi%' or description
like '%nisi%' or exists (select * from product_skus
where products
.id
= product_skus
.product_id
and (title
like '%nisi%' or description
like '%nisi%'))) limit 16 offset 0
订单模块开发
“订单”数据库表设计
先定义orders
表
字段名称 | 描述 | 类型 | 索引 |
---|---|---|---|
id | 自增长ID | unsigned int | 主键 |
no | 订单流水号 | varchar | unique |
user_id | 下单的用户ID | unsigned int | 外键 |
address | json格式的收货地址 | JSON | / |
total_amount | 订单总金额 | decimal | / |
remark | 订单备注 | text, null | / |
paid_at | 支付时间 | datetime, null | / |
payment_method | 支付方式 | varchar, null | / |
payment_no | 支付平台订单号 | varchar, null | / |
refund_status | 退款状态 | varchar | / |
refund_no | 退款单号 | varchar, null | 唯一 |
closed | 订单是否关闭 | tinyint, default 0 | / |
reviewed | 订单是否已经评价 | tinyint, default 0 | / |
ship_status | 物流状态 | varchar | / |
ship_data | 物流数据 | text, null | / |
extra | 其他额外数据 | text, null | / |
这里我们把收货地址用 JSON 格式保存而不是直接用一个外键连接到地址表,假如用户用地址 A 创建了一个订单,然后又修改了地址 A,那么用外键连接的方式这个订单的地址就会变成新地址,这并不符合正常的逻辑,所以我们需要用 JSON 格式把下单时的地址快照进订单,这样无论用户是修改还是删除之前的地址,都不会影响到之前的订单。
再定义order_items
表:
字段名称 | 描述 | 类型 | 索引 |
---|---|---|---|
id | 自增长ID | unsigned int | 主键 |
order_id | 所属订单ID | unsigned int | 外键 |
product_id | 对应商品ID | unsigned int | 外键 |
product_sku_id | 对应商品SKU的ID | unsigned int | 外键 |
amount | 数量 | unsigned int | / |
price | 单价 | decimal | / |
rating | 用户打分 | unsigned int, null | / |
review | 用户评价 | text, null | / |
reviewed_at | 评价时间 | timestamp, null | / |
给模型添加数据时直接和某模型关联
用associate
,仅仅对belongsTo
方法的模型有效, 如cart
-> belongsTo
-> user
$cart->user()->associate($user);
$cart->product_sku()->associate($sku_id);
减库存?
减库存的时候不能直接update(['stock' => $sku_stock - $amount])
,这样会导致负库存的发生。
public function decreaseStock($amount)
{
if ($amount < 0) {
throw new InternalException('减库存的数量不可小于0');
}
return $this->newQuery()->where('id', $this->id)
->where('stock', '>=', $amount)
->decrement('stock', $amount);
}
可以通过return返回的行数确定是否减库存成功
使用预加载和使用延迟加载有什么区别?
使用预加载一般都是在加载一批数据的时候,可以只需要一条...in (...)
的SQL语句就可以查出所要的数据,可以避免N+1
问题的出现,如官方代码:
$books = App\Book::with(['author.contacts'])->get();
而延迟加载一般用于一条数据的时候:
$books = App\Book::all();
if ($someCondition) {
$books->load('author', 'publisher'); // 单条数据加载关联
}
附上大神的解答:
就拿 Order 来说,在返回 Order 列表时需要用『预加载』,这个时候 Laravel 只需要一条 SQL 就能查出所有 Order 的 Items。
而『延迟预加载』通常在返回一条 Order 使用,就是你贴出来的这个代码,这种情况下你想用『预加载』也是可以的,效果是一样的。
使用Services模式
Service
模式将Controller
中的业务逻辑代码迁移至Service
类中,解决了因为业务逻辑量大和复杂导致的Controller
过于臃肿的问题,并且符合 SOLID
的单一责任原则。
单一职责原则:
如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。而如果想要避免这种现象的发生,就要尽可能的遵守单一职责原则。此原则的核心就是解耦和增强内聚性。
Service
还可以使用Laravel
的依赖注入功能,这样大大提高了Service
部分代码的测试性和程序的健壮性。
具体封装方式:https://laravel-china.org/courses/laravel-shop/5.5/encapsulation-business-code/1748
支付模块开发
获取关联模型与获取关联关系的区别
$order->items
$order->items()
二者的区别在于:
$order->items()
获取的是关联关系,这一步还没有做SQL
查询,通常是准备做进一步的查询;$order->items
获取的是关联的模型,SQL
已经查询完毕,已经从数据库中获取到关联的数据。比如一对多的
$order->items
获取到的就是一个Collection
集合,集合里的每个元素都是items
模型。
为何使用axios时不需要添加csrf_token?
在../resources/assets/js/bootstrap.js
中已经给出了答案:
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
优惠券模块开发
“优惠券”数据库表设计
设计表coupon_codes
字段名称 | 描述 | 类型 | 索引 |
---|---|---|---|
id | 自增ID | unsigned int | 主键 |
name | 优惠券的标题 | varchar | / |
description | 优惠券的基本信息描述 | varchar | / |
code | 优惠券码 | varchar | 唯一索引 |
type | 优惠券类型,支持固定金额和百分比折扣 | varchar | / |
value | 折扣值(百分比或者固定金额) | decimal | / |
total | 全站可兑换的数量 | unsigned int | / |
used | 全站已兑换的数目 | unsigned int | / |
min_amount | 使用该优惠券的最低订单金额 | decimal | / |
not_before | 优惠券开始使用时间 | datetime, null | / |
not_after | 优惠券截止时间 | datetime, null | / |
enabled | 优惠券是否可用(true or false) | tinyint | / |
然后为orders
表增加字段coupon_code_id
(此处先将订单捆绑优惠券)
字段名称 | 描述 | 类型 | 索引 |
---|---|---|---|
coupon_code_id | 优惠券id | unsigned int | 外键,对应coupon_codes 的id 字段 |
验证字段唯一性:编辑状态
某个字段的索引为唯一性索引,假如在编辑状态下未编辑此字段而提交的话,该字段的“验证”会报唯一性错误。
查看Laravel文档后得知:
unique:table,column,except,idColumn
强迫 Unique 规则忽略指定 ID :**
有时,你可能希望在进行字段唯一性验证时忽略指定 ID 。例如, 在「更新个人资料」页面会包含用户名、邮箱和地点。这时你会想要验证更新的 E-mail 值是否唯一。如果用户仅更改了用户名字段而没有改 E-mail 字段,就不需要抛出验证错误,因为此用户已经是这个 E-mail 的拥有者了。
使用
Rule
类定义规则来指示验证器忽略用户的 ID。这个例子中通过数组来指定验证规则,而不是使用 | 字符来分隔:use Illuminate\Validation\Rule; Validator::make($data, [ 'email' => [ 'required', Rule::unique('users')->ignore($user->id), ], ]);
如果你的数据表使用的主键名称不是
id
,那就在调用ignore
方法时指定字段的名称:'email' => Rule::unique('users')->ignore($user->id, 'user_id')
错误码
错误码 | 解释 |
---|---|
401 | 验证身份错误 |
400 | 未验证邮箱 |
422 | 表单校验错误 |
发表评论