SQL
新表格
为了实现美妙的课程订阅功能,需要一张新的表格,叫做course_enroll_records
,点击“加入课程”就会往表里面插入一行,包括课程号、用户号和一个判断是否为管理员类型的bool变量。
处理函数
定义了一些新的路由,以API为例,分别是
Route::group(['prefix' => 'course/{course}'], function () {
Route::post('check/student/{user?}', '[email protected]')->name('api.forum.course.check.student');
Route::post('check/admin/{user?}', '[email protected]')->name('api.forum.course.check.admin');
Route::post('add/student', '[email protected]')->name('api.forum.course.add.student.myself');
Route::post('delete/user', '[email protected]')->name('api.forum.course.delete.user.myself');
Route::group(['middleware' => 'admin'], function () {
// Only admin can add admin, and control other users.
Route::post('add/student/{user?}', '[email protected]')->name('api.forum.course.add.student');
Route::post('add/admin/{user?}', '[email protected]')->name('api.forum.course.add.admin');
Route::post('delete/user/{user?}', '[email protected]')->name('api.forum.course.delete.user');
});
});
可以看出,addStudent
和deleteUser
分别有用户版和管理员两个版本,区别就是用户版只能修改自己,而管理员版可以带个参数,来修改其他人。对应的处理函数会调用模型的methods,从而实现数据库的操作。而web版的处理函数则是在CourseController
中新建一个protected $courseAPI;
实例,直接偷懒调用API的函数,反而写起来轻松,维护也好弄。
联合查询
然而接下来就遇到了一个难题。如何对多张表的数据进行同时查询呢?需要实现的查询需求如下:
首先从
assignments
表中把所有没截止的作业拿出来,然后如果issuer
一列为对应的用户ID,则选中这列,或者如果对应的course_id
和用户ID在course_enroll_records
表中有命中,则选中。
怎么办呢?
上网一查,只需要做联合就可以了(这里不介绍了)。
那么怎么写呢?(省略半天研究+查资料)
public function scopeSubscribedByUser($query, $userID)
{
return $query->join('course_enroll_records', function ($join) use ($userID) {
$join->where('course_enroll_records.user_id', $userID)
->on(function ($join) use ($userID) {
$join->where('assignments.issuer', $userID)
->orOn(function ($join) {
$join->where('assignments.issuer', 0)
->on('assignments.course_id', '=', 'course_enroll_records.course_id');
});
});
})->distinct('assignments.id'); // holy [email protected]!
}
这TM什么鬼?
首先,这是一个scope函数,是可以直接接到查询语句里($query->subscribedByUser($user)
)作为一整段查询条件(其实就是个好多层的lambda表达式)使用的。当选择assignments
表后,这个过程会先会把assignments
表和course_enroll_records
表合并,选择满足以下条件的行联合起来:
-
course_enroll_records
中的user_id
列与$userID
相等。 -
assignments
中的issuer
列与$userID
相等,或者issuer
列为0且两个表的course_id
相等。
这样就可以把issuer!=0
或者是用户订阅了课程的作业筛选出来啦!
Bug1:ID被覆盖
然而这样筛选出来之后通过$assignment->id
获取到的编号竟然是course_enroll_records.id
,也就是说作业的ID被覆盖了。
解决方法是使用SELECT assignments.*
(选择作业表部分的内容),而不是SELECT *
(同名列会被覆盖)。
Bug2:空行重复
其实这个bug是下午4点多才测出来的。一开始测试的时候只订阅了一个课程,但是当订阅了两个课程的时候,个人作业(issuer!=0
)就会重复出现。
造成bug的原因是SQL会把连接的右边表进行遍历,结果发现两个订阅都符合条件2的前半部分(后半部分就短路了),然后就出现了重复的内容。解决方案是加一层SELECT DISTINCT *
,也就是上面scope函数的最后一行。
axios
axios与AJAX
AJAX是jQuery的一部分,但是jQuery其实是一个很大的库,而且AJAX用起来并不是很方便。主要原因是,Laravel对网页有CSRF保护,POST类型必须把CSRF-Token作为一个输入放在头表里提交上去,否则就会出现419错误(耐人寻味的数字)。然而Laravel在默认的脚本中已经配好了axios的CSRF-Token头表,直接调用就可以了。
axios其实是Vue的组件,并且基于promise,符合ES6的语法标准,但是我在开发的时候完全没有用到Vue,反而是把jQuery+AJAX替换成了jQuery+axios, jQuery在改变页面内容上很方便,axios在进行交互请求方面很方便,两者一结合,boom,太爽了。(当然少不了JS各种出错让我哭)
How to use
axios其实很简单……
axios.post(url, {data})
.then(function (response){
// success
})
.error(function (error){
// fail
});
就这么多……axios因为使用promise,整个异步处理简单清晰,很好看懂,比AJAX的写法清爽很多。
How to implement
$('.xxBtnContainer').on('click', '.xxBtn', function() {
let api = this.dataset.api;
let pid = this.dataset.pid;
axios.post(url, {})
.then(function (response) {
$('#.xxBtnContainer-' + pid).html(response.data.xxBtn_html);
})
.error(function (error) {
$.alert(/*...*/);
});
});
挺容易懂的,每个可按下的按钮都有一个配套的container,监听按下按钮的事件。按钮按下后首先从`data-xxx
属性中取出对应的数据,然后进行一次axios访问。成功后通过获取的数据把原来的按钮更新掉。
这种container+button写法的好处是,可以直接对整个按钮进行替换,服务器端只需要直接调用属性,把新的元素整个返回即可,而客户端也不会因为旧的按钮被替换而无法监听新的事件。