商城项目-后台商品发布/前端页面引入/前端用户登录验证中间件/生成订单
学习心得
要学习朱老师用”模块化”的思想编写前端页面组件, 实现代码复用.
laravel的用户验证, 西门老师只领进门, 更多使用还得看手册.
巧妙的添加查询条件, 有效的解决大量数据的读取的效率问题.
1. 后台商品发布/前端页面引入
1.1 发布商品时, 如何加载商品分类
- 之前老师介绍过, 为消灭关联查询, 把要关联的表, 以
id
做数组key
的方式查询出来. 这种方式, 对要关联的表, 数据量少的时候, 可以适用. 当数据量很多时, 比如, 淘宝的商品分类表, 就有好几百兆的数据, 没有必要全查出来. - 为解决这个问题, 可以在查询出商品分页数据后, 把这些分页商品的分类id做成in参数, 在查询以id做数据key的数组时, 适用in进行过滤, 这样可以减少查询时间消耗.
- 在新增商品界面, 如果要加载商品分类, 假设商品分类很多, 可以加一个搜索框, 通过搜索框输入的内容异步的去过滤查询商品分类项, 返回的数据拼接字符串形成
<select>
元素的<option>
列表并渲染即可. 记得最后要form.render('select')
.- 模糊查询要用
like
, 当分类很多时, 数据库是撑不住的.此时就需要用搜索引擎了, 如:sphinx
,elasticsearch
. - 模糊查询, 可能会匹配到很多数据, 所以查询一定要加限制返回的行数, 像百度搜索
laravel文档
, 其实也只返回权重较高的730条数据而已, 但匹配到的数据上百万条.
- 模糊查询要用
- 之前老师介绍过, 为消灭关联查询, 把要关联的表, 以
大量数据的的读取方式
- 启用缓存. 如Redis, memerycache等. 先从缓存中读取数据, 如果读取不到, 再冲数据库中获取, 然后保存到缓存中.
1.2 前端页面引入
2. 前端用户登录验证中间件
2.1 创建前端登录验证中间件
- 老师总结的三步骤: 创建, 注册和触发.
2.2 使用laravel提供的验证机制进行前端用户登录验证
- 照葫芦画瓢, 仿照
User
模型创建一个Member
模型, 绑定数据库中的前端用户表member
.
- 照葫芦画瓢, 仿照
- 先不用理解, 照老师的讲解, 在
/config/auth.php
中照葫芦画瓢创建一个”防守人”(guard
)
- 先不用理解, 照老师的讲解, 在
- 实现前端登录验证中间件并注册之. 在其
handle
方法中, 使用Auth::guard('member')->guest()
执行登录验证. 使用guard()
函数指定”防守人”, 如果不调用函数, 则使用laravel默认的”防守人”(user
).guest()
方法则是执行登录验证. 未登录则返回true
.
- 实现前端登录验证中间件并注册之. 在其
- 同样, 在路由中需要使用前台登录验证中间件的路由上调用
middleware()
方法保护路由.
- 同样, 在路由中需要使用前台登录验证中间件的路由上调用
- 如果是前后端分离的项目, 假设项目前端和后端不在同一个域名/服务器中, 因为
session
不能跨服务器共享的原因, 可以改用/config/auth.php
中声明的另一种驱动方式(对应路由:api.php
):driver => 'token
. 具体使用方法要去看文档.
- 如果是前后端分离的项目, 假设项目前端和后端不在同一个域名/服务器中, 因为
tips: 多表更新/插入操作, 是否需要加事务
理论上是要加的, 但会损失部分的性能
但是在实际开发中, 如果订单表(插入)和产品表(减库存)不在同一个数据库服务器, 会比较麻烦, 比较难处理, 索性不加.
生成订单
使用
Auth::guard('member')->user()
获取前端用户信息.价格是很敏感的字段值, 前端传入的价格不可信, 必须根据商品id从数据库中查询价格.
生成不重复的订单号的方法: 时间戳+用户id+随机数1+随机数2生成订单号, 这样拼接成的订单号出现重复号的概率很低了.
代码清单(部分)
- 1- 新增商品界面(关注点: 商品分类筛选)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>新增产品</title>
<link rel="stylesheet" href="/static/plugin/layui/css/layui.css">
<script src="/static/plugin/layui/layui.js"></script>
<!-- ueditor-start -->
<!-- 配置文件 -->
<script type="text/javascript" src="/static/plugin/ueditor/ueditor.config.js"></script>
<!-- 编辑器源码文件 -->
<script type="text/javascript" src="/static/plugin/ueditor/ueditor.all.js"></script>
<!-- ueditor-end -->
<link rel="stylesheet" href="/static/css/product.css">
<style>
.hide {
display: none;
}
#cwords, .cid {
margin-left: 10px;
}
</style>
</head>
<body>
<div class="layui-form">
@csrf
<div class="layui-form-item">
<label for="title" class="layui-form-label">产品名称</label>
<div class="layui-input-block">
<input type="text" class="layui-input" name="title" id="title" value="">
</div>
</div>
<div class="layui-form-item">
<label for="cid" class="layui-form-label">产品分类</label>
<div class="layui-input-inline">
<input type="text" class="layui-input" name="cwords" id="cwords" oninput="search_cates()">
</div>
<div class="layui-input-inline cid">
<select name="cid" id="cid"></select>
</div>
</div>
<div class="layui-form-item">
<label for="thumb" class="layui-form-label">缩略图</label>
<div class="layui-input-block">
<button type="button" class="layui-btn" id="upload">
<i class="layui-icon"></i>上传图片
</button>
</div>
</div>
<div class="layui-form-item">
<label for="subtitle" class="layui-form-label">子标题</label>
<div class="layui-input-block">
<input type="text" class="layui-input" name="subtitle" id="subtitle" value="">
</div>
</div>
<div class="layui-form-item">
<label for="keywords" class="layui-form-label">关键字</label>
<div class="layui-input-block">
<input type="text" class="layui-input" name="keywords" id="keywords" value="">
</div>
</div>
<div class="layui-form-item">
<label for="descs" class="layui-form-label">描述</label>
<div class="layui-input-block">
<input type="text" class="layui-input" name="descs" id="descs" value="">
</div>
</div>
<div class="layui-form-item">
<label for="price" class="layui-form-label">单价</label>
<div class="layui-input-block">
<input type="number" name="price" id="price" class="layui-input" value="0">
</div>
</div>
<div class="layui-form-item">
<label for="stock" class="layui-form-label">库存</label>
<div class="layui-input-block">
<input type="number" name="stock" id="stock" class="layui-input" value="0">
</div>
</div>
<div class="layui-form-item">
<label for="pv" class="layui-form-label">浏览量</label>
<div class="layui-input-block">
<input type="number" name="pv" id="pv" class="layui-input" value="0">
</div>
</div>
<div class="layui-form-item">
<label for="status_0" class="layui-form-label">商品状态</label>
<div class="layui-input-block">
<input type="radio" name="status" id="status_0" value="0" title="未上架" checked>
<input type="radio" name="status" id="status_1" value="1" title="已上架">
</div>
</div>
<div class="layui-form-item">
<label for="content" class="layui-form-label">商品描述</label>
<div class="layui-input-block">
<!-- 加载编辑器的容器 -->
<!-- 实际生成的是div. 给div添加contenteditable="true", 这个div就能编辑 -->
<script id="container" name="content" type="text/plain">这里写你的初始化内容</script>
</div>
</div>
</div>
<div id="tong" class="hide">
<img src="" style="max-width: 100%; max-height: 100%">
</div>
</body>
<script>
layui.use(['layer', 'form', 'upload'], function() {
layer = layui.layer;
form = layui.form;
upload = layui.upload;
$ = layui.jquery;
//执行实例
var uploadInst = upload.render({
elem: '#upload' //绑定元素
, url: '/admin/upload/pic_upload' //上传接口
, multiple: true // 多文件上传
, data: {
_token: $('input[name="_token"]').val()
},
done: function(res) {
//上传完毕回调
$('<img name="preview_img" src="' + res.data.src + '" alt="" style="height: 36px; margin-right: 5px;" onclick="big_img(this)">').appendTo($('#upload').parent());
},
error: function() {
//请求异常回调
}
});
// 实例化编辑器
/* 第二个参数是配置属性对象 */
/* 不要写var, 把ue声明为全局变量, 因为其他地方也用到ue对象 */
/* var */
ue = UE.getEditor('container', {
initialFrameWidth: '100%', //初始化编辑器宽度,默认1000
initialFrameHeight: '500' //初始化编辑器高度,默认320
});
});
function save() {
var data = {};
data._token = $('input[name="_token"]').val();
data.title = $.trim($('#title').val());
data.cid = $('#cid').val();
data.subtitle = $.trim($('#subtitle').val());
data.keywords = $.trim($('#keywords').val());
data.descs = $.trim($('#descs').val());
data.price = parseInt($('#price').val());
data.stock = parseInt($('#stock').val());
data.pv = parseInt($('#pv').val());
data.status = $('input[name="status"]:checked').val();
data.content = ue.getContent();
// 缩略图
var thumbs = [];
$('img[name="preview_img"]').each(function(index, item) {
thumbs.push(item.src);
});
data.thumb = thumbs.join(',');
// 必填项判空
var check = inputCheck(data, {title: '标题', cid: '商品分类', keywords: '关键字', descs: '描述', price: '单价', content: '商品描述'});
if(check.status) {
return layer.alert(check.message, {icon: 2});
}
// 两个数字域的值判断
if(isNaN(data.price)) {
return layer.alert('单价必须填数字', {icon: 2});
}
if(isNaN(data.pv)) {
return layer.alert('浏览量必须是数字', {icon: 2});
}
// 提交保存
$.post(
'/admin/product/save'
, data
, function(res) {
if(res.status != undefined && res.status == '0') {
layer.msg(res.message, {icon: 1});
setTimeout(() => {
window.parent.location.reload();
}, 1000);
} else if(res.status != undefined) {
layer.alert(res.message, {icon: 2});
} else {
layer.alert('提交保存失败', {icon: 2});
}
}
, 'json'
);
}
// 判空
function inputCheck(data, checkItem) {
for(key in checkItem) {
if(data[key] == undefined || data[key] == '') {
$('input[name="'+key+'"]').focus();
return {status: 1, message: checkItem[key] + '不能为空'};
}
}
return {status: 0};
}
// 商品分类关键字搜索
function search_cates() {
var search_words = $.trim(event.target.value);
if(search_words == '') return;
$.get(
'/admin/product/search_product_cate'
, {words: search_words}
, function(res) {
if(res.status != undefined && res.status == 0) {
var str = '<option></option>';
res.data.forEach(function(item, index) {
str += '<option value="'+item.id+'">'+item.title+'</option>'
});
$('#cid').html('');
$(str).appendTo('#cid');
form.render('select');
}
}
, 'json'
);
}
// 查看大图
function big_img(img) {
if (img.src == undefined || img.src == "") {
return;
}
$('#tong > img').attr('src', img.src);
//页面层-图片
layer.open({
type: 1,
title: false,
closeBtn: 0,
area: ['auto'],
skin: 'layui-layer-nobg', //没有背景色
shadeClose: true,
content: $('#tong')
});
}
</script>
</html>
- 2-
Member
模型
<?php
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class Member extends Authenticatable
{
use Notifiable;
// 绑定的数据库表名
protected $table = 'member';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
}
- 3-
/config/auth.php
中增加”防守人”配置
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
// 照葫芦画瓢, 增加一个供前端登录验证中间件使用的防守人
'member' => [
'driver' => 'session',
'provider' => 'members',
]
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// 照葫芦画瓢, 添加'members'
'members' => [
'driver' => 'eloquent',
'model' => App\Member::class,
]
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expire time is the number of minutes that the reset token should be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the amount of seconds before a password confirmation
| times out and the user is prompted to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => 10800,
];
- 4- 前端登录验证中间件
<?php
namespace App\Http\Middleware;
use App\Http\helper\CodeHelper;
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
/* 前端验证登录中间件 */
class AuthMember {
public function handle($request, Closure $next, $guard = null) {
// guard()方法是指定使用哪个"防守人"来验证; 不调用guard()方法, 则默认为'user';
// guest()方法返回一个布尔值, 为真时, 表示当前没有用户登录
if(Auth::guard('member')->guest()) { // 验证失败了, 用户没登录
// 如果是ajax请求, 则返回相应
if($request->ajax()) {
$response = ['status' => 401, 'message' => '请先登录'];
return response(json_encode($response), 200);
}
// 否则, 重定向到'member/login'路由
return redirect()->guest('member/login');
}
return $next($request);
}
}
- 注册中间件和路由略.
- 商品详情页(关注点: 立刻购买逻辑)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- 当前文档要用到阿里字体图标-->
<link rel="stylesheet" href="/static/font/iconfont.css">
<link rel="stylesheet" href="/static/css/shop/shop_detail.css">
<title>商城详情页</title>
<script src="/static/js/jquery3.4.1.js"></script>
<link rel="stylesheet" href="/static/plugin/layui/css/layui.css">
<script src="/static/plugin/layui/layui.js"></script>
</head>
<body>
<!--公共页眉-->
@include('/front/public/header')
<!--主体全部放在main元素中-->
<main>
<!-- 商城公共头部-->
<!--logo+搜索框+快捷入口区-->
@include('/front/public/header_search')
<!--为商品详情区块单独创建一个包含块,方便用网格布局-->
<div class="detail">
<!--商城详情页上部购买组件-->
<div class="shop-detail-bug">
<!--头部面包屑导航-->
<nav>
<a href="">首页 > </a>
<a href="">图片写真 > </a>
<a href="">日本 > </a>
<a href="">颖宝宝</a>
</nav>
<article>
@csrf
<input type="hidden" name="id" value="{{$product['id']}}">
<span><img src="{{$product['thumb']}}" alt=""></span>
<div>
<!--商品标题-->
<h3>{{$product['title']}}</h3>
<!--商品价格-->
<div class="price">
<span>本站特惠:</span>
<span>¥{{$product['price']}}</span>
</div>
<!--基本描述-->
<div class="desc">
销量: <span>13</span> |
累积评价: <span>3</span> |
好评率: <span>199%</span>
</div>
<!-- 购买数量-->
<div class="buy-num">
<label for="num">购买数量:</label><input type="number" id="num" value="1">
</div>
<!--购买按钮-->
<div class="buy-btn">
<button type="button" onclick="buy()">立即购买</button>
<button><i class="iconfont icon-icon_tianjia"></i>加入购物车</button>
</div>
<!--售后承诺-->
<div class="promise">
<span><i class="iconfont icon-zhanghaoquanxianguanli"></i>本站保障</span>
<span><i class="iconfont icon-icon_safety"></i>企业认证</span>
<span><i class="iconfont icon-tianshenpi"></i>退款承诺</span>
<span><i class="iconfont icon-kuaisubianpai"></i>免费换货</span>
</div>
</div>
</article>
</div>
<!--商城详情页左下推荐商品列表-->
<div class="shop-detail-recommend">
<h3>推荐商品</h3>
<div>
<a href="">
<img src="/static/images/shop/shop1.jpg" alt="">
</a>
<a href="">韩国美女最新海报促销美妆写真图集</a>
<div class="hot">
<span>热销:</span><span>8976</span>
<span>价格:</span><span>¥99</span>
</div>
</div>
<div>
<a href="">
<img src="/static/images/shop/shop2.jpg" alt="">
</a>
<a href="">韩国美女最新海报促销美妆写真图集</a>
<div class="hot">
<span>热销:</span><span>324</span>
<span>价格:</span><span>¥798</span>
</div>
</div>
<div>
<a href="">
<img src="/static/images/shop/shop3.jpg" alt="">
</a>
<a href="">韩国美女最新海报促销美妆写真图集</a>
<div class="hot">
<span>热销:</span><span>678</span>
<span>价格:</span><span>¥630</span>
</div>
</div>
<div>
<a href="">
<img src="/static/images/shop/shop4.jpg" alt="">
</a>
<a href="">韩国美女最新海报促销美妆写真图集</a>
<div class="hot">
<span>热销:</span><span>12</span>
<span>价格:</span><span>¥980</span>
</div>
</div>
</div>
<!--商城详情页右下详情选项卡-->
<div class="shop-detail-tab">
<div class="tab">
<span class="active">商品详情</span>
<span>案例/演示</span>
<span>常见问题</span>
<span>累计评价</span>
<span>产品咨询</span>
</div>
<div class="content">{!!$product['content']!!}</div>
</div>
<!--评论与回复-->
<div class="public-comment-reply">
<!-- 评论区-->
<div class="comment">
<h3>我要评论</h3>
<div>
<label for="comment"><img src="/static/images/user.png" alt=""></label>
<textarea name="" id="comment"></textarea>
</div>
<button>发表评论</button>
</div>
<!-- 回复区-->
<div class="reply">
<h3>最新回复</h3>
<div>
<img src="/static/images/user.png" alt="">
<div class="detail">
<span>用户昵称</span>
<span>留言内容: php中文网,是一个有温度,有思想的学习平台</span>
<div>
<span>2019-12-12 15:34:23发表</span>
<span><i class="iconfont icon-dianzan"></i>回复</span>
</div>
</div>
</div>
<div>
<img src="/static/images/user.png" alt="">
<div class="detail">
<span>用户昵称</span>
<span>留言内容: php中文网,是一个有温度,有思想的学习平台</span>
<div>
<span>2019-12-12 15:34:23发表</span>
<span><i class="iconfont icon-dianzan"></i>回复</span>
</div>
</div>
</div>
<div>
<img src="/static/images/user.png" alt="">
<div class="detail">
<span>用户昵称</span>
<span>留言内容: php中文网,是一个有温度,有思想的学习平台</span>
<div>
<span>2019-12-12 15:34:23发表</span>
<span><i class="iconfont icon-dianzan"></i>回复</span>
</div>
</div>
</div>
<div>
<img src="/static/images/user.png" alt="">
<div class="detail">
<span>用户昵称</span>
<span>留言内容: php中文网,是一个有温度,有思想的学习平台</span>
<div>
<span>2019-12-12 15:34:23发表</span>
<span><i class="iconfont icon-dianzan"></i>回复</span>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!--公共页脚-->
@include('/front/public/footer')
</body>
<script>
layui.use(['layer'], function() {
layer = layui.layer;
});
function buy() {
$data = {};
$data.id = parseInt($('input[name="id"]').val());
$data._token = $('input[name="_token"]').val();
$data.buy_count = $("#num").val();
if(isNaN($data.id)) {
return layer.alert('参数错误', {icon: 2});
}
// 下订单
$.post(
'/shop/create_order'
, $data
, function(res) {
if(res.status != undefined && res.status == 401) {// 用户未登录
layer.open({
type: 2,
title: '登录',
shadowClose: false,
shadow: 0.5,
area: ['400px', '250px'],
content: '/account/login'
});
} else if(res.status != undefined && res.status == '0') {// 下单成功
layer.alert(res.message, {icon: 1});
layer.open({
type: 2,
title: '扫码支持',
shadowClose: false,
shadow: 0.5,
area: ['400px', '400px'],
content: '/shop/pay'
});
} else if(res.status != undefined) {
return layer.alert(res.message, {icon: 2});
}
}
, 'json'
);
}
</script>
</html>
- 6- 生成订单的控制器方法
// 下订单
public function createOrder(Request $req) {
// 登录用户
$member = Auth::guard('member')->user();
// 商品id
$pro_id = (int) $req->id;
$product = DB::table('product')->where([['id', $pro_id], ['status', 1]])->getFirst();
if(!$product) {
return CodeHelper::failure_json('该商品不存在或未上架');
}
// 无货或供货不足
if($product['stock'] < 0 || $product['stock'] < $req->buycount) {
return CodeHelper::failure_json('库存不足');
}
// 使用时间戳+用户id+随机数1+随机数2生成订单号(这样拼接成的订单号出现重复号的概率很低了)
$data['ord_no'] = time() . $member->id . rand(100, 500) . rand(501, 999);
// 购买人id
$data['member_id'] = (int) $member->id;
// 商品id
$data['pro_id'] = $pro_id;
// 购买数量
$data['count'] = $req->buy_count;
// 金额
$data['money'] = $req->buy_count * $product['price'];// 一定要从数据库里获取商品价格, 前端传过来的价格都不可信
//
$data['add_time'] = time();
// 保存订单
$oid = DB::table('orders')->insertGetId($data);
// 减库存(用laravel提供的decrement()方法减库存)
DB::table('product')->where('id', $pro_id)->decrement('stock', $req->buy_count);
return CodeHelper::success_json('下单成功');
}