diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..8f0de65
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yml,yaml}]
+indent_size = 2
+
+[docker-compose.yml]
+indent_size = 4
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..d5c399e
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,52 @@
+APP_NAME=moon-shop
+APP_ENV=product
+APP_KEY=
+APP_DEBUG=true
+APP_LOG_LEVEL=debug
+# 请务必修改此项为自己的 URL
+APP_URL=http://shop.shiguopeng.cn
+
+# 数据库设置
+DB_CONNECTION=mysql
+DB_HOST=
+DB_PORT=3306
+DB_DATABASE=
+DB_USERNAME=
+DB_PASSWORD=
+
+
+# 缓存, session, 队列驱动设置
+BROADCAST_DRIVER=log
+CACHE_DRIVER=file
+SESSION_DRIVER=file
+QUEUE_DRIVER=database
+
+# redis 数据库配置
+REDIS_HOST=
+REDIS_PASSWORD=
+REDIS_PORT=6379
+
+# 邮箱设置
+MAIL_DRIVER=smtp
+MAIL_HOST=smtp.qq.com
+MAIL_PORT=465
+MAIL_USERNAME=
+MAIL_PASSWORD=
+MAIL_FROM_ADDRESS=
+MAIL_FROM_NAME=MondayShop
+MAIL_ENCRYPTION=ssl
+
+# 第三方互联登录
+OAUTH_GITHUB_ID=
+OAUTH_GITHUB_SECRET=
+OAUTH_QQ_ID=
+OAUTH_WEIBO_ID=
+
+# 支付宝支付
+ALIAPY_APP_ID=
+
+
+JWT_SECRET=
+
+# Docker 运行必须是 0.0.0.0
+LARAVELS_LISTEN_IP=0.0.0.0
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..fcb21d3
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+* text=auto eol=lf
+
+*.blade.php diff=html
+*.css diff=css
+*.html diff=html
+*.md diff=markdown
+*.php diff=php
+
+/.github export-ignore
+CHANGELOG.md export-ignore
+.styleci.yml export-ignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b2313e6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,20 @@
+/.phpunit.cache
+/node_modules
+/public/build
+/public/hot
+/public/storage
+/storage/*.key
+/vendor
+.env
+.env.backup
+.env.production
+.phpunit.result.cache
+Homestead.json
+Homestead.yaml
+auth.json
+npm-debug.log
+yarn-error.log
+/.fleet
+/.idea
+/.vscode
+rr
diff --git a/README.md b/README.md
index 699d866..6b16e09 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,306 @@
-# aquaculture
+# seth-shi/monday-shop
+* !!! 非`Docker`运行请使用`v1`[https://github.com/seth-shi/monday-shop/tree/1.0](https://github.com/seth-shi/monday-shop/tree/1.0)
+
+## QQ 群
+* `584453488`
+
+## 部署(新新新)
+1. 下载源码(也可直接下载压缩包, 然后解压)
+ * `git clone https://github.com/seth-shi/monday-shop.git`
+2. 修改配置
+ * `cp .env.example .env`
+ * 修改`.env`文件中数据库,域名等配置信息
+3. 构建镜像
+ * `docker build . -t monday-shop`
+4. 运行
+ * `docker run -d --net=host --name monday-shop-service monday-shop`
+5. 执行安装命令
+ * `docker exec monday-shop-service php artisan moon:install`
+
+## 目录说明
+* [演示地址](#演示地址)
+* [页面展示](#页面展示)
+* [特色](#Feature)
+* [安装](#Installation)
+* [命令行功能](#Commands)
+* [秒杀处理逻辑](#秒杀处理逻辑)
+* [API文档(新)](#API)
+* [依赖的 Packages](#Packages)
+* [文章引用](#Reference)
+* [错误/注意点](#Notice)
+* [协议](#License)
+
+## 演示地址
+[演示地址:http://shop.shiguopeng.cn](http://shop.shiguopeng.cn)
+
+[后台地址:http://shop.shiguopeng.cn/admin](http://shop.shiguopeng.cn/admin)
+* 账号:`admin`
+* 密码:`admin`
+****
+* 测试支付功能
+ * 下载[支付宝沙箱](https://sandbox.alipaydev.com/user/downloadApp.htm)
+ * 账号:`eyxweq5099@sandbox.com`
+ * 密码:`111111`
+ * 支付密码:`111111`
+ * 当前账户余额:`9999999.99`
+ * 余额不足,请联系我及时充值
+
+## 页面展示
+
+
+
+
+
+
+
+
+
+## Feature
+- [x] **库存问题**
+ * [x] 普通订单使用乐观锁防止超卖
+ * [X] 秒杀订单使用`Redis`队列防止超卖
+- [x] **首页数据全走缓存(推荐使用`Redis`驱动)**
+ * [x] 未登录的首页,零数据库查询,通过缓存驱动
+ * [x] 计划任务每分钟会更新一次首页数据
+ * [x] 开启秒杀模块,零数据库查询,通过`Redis`驱动
+ * [x] 登录之后首页零数据库查询,`Session`驱动数据
+- [x] **积分功能**
+ * [x] 每日首次登录(访问网站)得到积分(bitmap 计算)
+ * [x] 连续登录 n 天得到积分(bitmap 计算)
+ * [x] 当天浏览商品数量 n 个得到积分(bitmap 计算)
+ * [x] 后台可新增 n+ 积分种类
+ * [x] 完成订单可获得金钱等比例积分
+- [x] **优惠券功能**
+ * [x] 满减优惠
+ * [x] 积分兑换满减优惠券
+ * [x] 发放兑换码,兑换优惠券
+- [x] **物流功能**
+ * [x] 运费设置
+ * [x] 快递物流
+- [x] **秒杀功能**
+ * 秒杀过期,自动回退库存
+ * 使用延时队列,当订单超过三十分钟(可配置)未付款,自动取消订单
+ * 秒杀商品,如果用户收藏,发送邮件提醒活动
+ * 后台秒杀模块的开启关闭
+ * 秒杀的商品数量,皆通过`Redis`读取
+- [x] **第三方授权登录 + 登录回跳**
+ * `Github`
+ * `QQ`
+ * 微博
+- [x] **第三方支付(支持自动适应手机,web 支付)**
+ - [x] 支付宝支付,退款
+ - [ ] 微信支付
+- [x] **购物车**
+ * 使用`H5`本地存储
+ * 登录之后同时显示本地购物车和数据库购物车数量
+ * 用户登录之后会询问是否需要持久化到数据库
+- [x] **商品搜索**
+ * 使用**ElasticSearch**全文索引
+ * 支持拼音首字母
+ * `AJAX`无刷新显示
+- [x] **订阅模块**
+ * 每周定时推送一封邮件包含最受欢迎,最新,最火卖商品
+- [x] **分类排序**
+ * 后台使用拖动排序,可以设置在商城首页优先展示的分类
+- [x] **订单模块**
+ * 订单下单
+ * 买家支付
+ * 后台发货 / 卖家申请退款
+ * 买确认收货 / 后台确认收货
+ * 买家确认订单获取积分
+ * 用户下订单之后可以评论
+- [x] **站内消息**
+ * 消息通知
+ * 多模板类型通知, 兑换码通知、文章通知等等
+ * 轮询通知消息,一点即达
+- [x] **数据统计**
+ * 每天晚上一点进行站点数据统计
+- [x] 全文搜索
+- [x] **响应式网站**
+
+
+## Commands
+| 命令 | 一句话描述 |
+| ----- | --- |
+|`php artisan moon:install`|安装应用程序|
+|`php artisan add:shop-to-search`|生成全文索引|
+|`php artisan moon:uninstall`|卸载网站(清空数据库,缓存,路由)|
+|`php artisan moon:cache`|执行缓存(缓存配置,路由,类映射)|
+|`php artisan moon:clear`|清除缓存|
+|`php artisan moon:copy`|复制项目内置的静态资源|
+|`php artisan moon:delete`|删除项目及上传的基本静态资源|
+|`php artisan moon:export`|导出用户数据到json文件|
+|`php artisan moon:count-site`|统计站点任务(每天夜里一点执行)|
+|`php artisan moon:del-seckills`|删除秒杀数据 (每小时自动执行一次)|
+|`php artisan moon:moon:del-score-data`|删除积分缓存数据 (每天夜里 0 点执行)|
+|`php artisan moon:update-home`|更新首页数据 (每分钟自动执行一次)|
+|`php artisan moon:send-subscribes`|发送订阅邮件 (每个礼拜六早上八点)|
+|`php artisan queue:work --tries=3`|监听队列(邮件发送,处理过期的秒杀数据 !!!|
+
+## 秒杀处理逻辑
+```php
+
+## 初始化抢购数据
+number, 9));
+
+?>
+
+## 抢购
+id();
+
+// 判断是否已经开始了秒杀
+
+// 返回 0,代表当前用户已经抢购过了
+if (0 == Redis::hset("seckills:{$id}:users:{$userId}", 'id', $userId)) {
+
+ return responseJson(403, '你已经抢购过了');
+}
+
+// 如果从队列中读取到了 null,代表已经没有库存
+if (is_null(Redis::lpop("seckills:{$id}:queue"))) {
+
+ return responseJson(403, '已经抢购完了');
+}
+
+// 这里就可以开始入库订单
+
+?>
+
+## 利用 crontab 定时扫描过期数据,回滚库存,删除过期 redis (可选)
+where('end_at', '<', date('Y-m-d H:i:s'))
+ ->get()
+ ->map(function (Seckill $seckill) {
+
+ // 先模糊查找到所有用户 key
+ $ids = Redis::keys("seckills:{$seckill->id}:*");
+ Redis::del($ids);
+
+ // 回滚库存
+ // 做更多的事
+ };
+
+?>
+
+```
+
+## API
+* 接口响应数据说明
+ * 响应的数据格式总是保证拥有基本元素(`code`, `msg`, `data`)
+ * `code` 请参考接口全局状态码说明
+ * `msg` 此次请求消息,如果返回状态码为非成功,可直接展示`msg`
+ * `data` 如果为列表页将会一个数组类型(如商品列表),否则为一个对象类型(商品详情)
+ * 如有额外扩展字段, 将于基本元素平级, 如分页的`count`
+```json
+{
+ "code": 401,
+ "msg": "无效的token",
+ "data": []
+}
+```
+* 刷新`token`说明
+ * 为了保证安全性,`token`的有效时间为`60`分钟
+ * 当旧的`token`失效时,服务器会主动刷新,并在响应头加入`Authorization`
+ * 这时候旧的`token`将会加入黑名单不能再使用, 请将在响应头中新的`token`保存使用
+ * 当服务器主动刷新之后,会有一个期限(`2`周).服务器将无法再刷新,将返回`402`状态码,请重新登录账户
+* `token`使用流程说明
+```javascript
+// 全局请求类
+function request(_method, _url, _param, _func) {
+
+ $.ajax({
+ method: _method,
+ url: _url,
+ data: _param,
+ beforeSend: function (xhr) {
+ console.log(xhr);
+ xhr.setRequestHeader('Authorization', localStorage.getItem('api_token'))
+ },
+ complete: function (xhr, a, b) {
+
+ if (xhr.getResponseHeader('Authorization')) {
+ localStorage.setItem('api_token', xhr.getResponseHeader('Authorization'))
+ }
+ },
+ success: function (res) {
+
+ // token 永久过期
+ if (res.code === 402) {
+ // 跳去登录页面
+ return false;
+ }
+ // 更多状态码判断
+ }
+ });
+}
+
+// 第一次登录保存 token, 之后使用全局类请求数据即可
+
+```
+
+* 接口全局状态码说明(建议封装一个全局请求类或者中间件,统一处理全局状态码)
+ * `200`
+ * 请求数据成功
+ * `401`
+ * 身份验证出错(未登录就请求数据)
+ * 非法无效的`token`
+ * `token`已被加入黑名单(一般不会出现这个问题,出现这个问题那么就是你刷新 token 的逻辑有问题)
+ * `402`
+ * `token`已完全失效,后台暂设为 2 周,再也无法刷新,请重新登录账户
+ * `500`
+ * 服务器出错,具体请参考响应的消息
+* __接口文档__(重要的事情说三遍)
+
+[接口文档](http://shop.shiguopeng.cn/docs.html)
+
+[接口文档](http://shop.shiguopeng.cn/docs.html)
+
+[接口文档](http://shop.shiguopeng.cn/docs.html)
+
+
+## Packages
+| 扩展包 | 一句话描述 | 在本项目中的使用案例 |
+| --- | --- | --- |
+|[z-song/laravel-admin](https://github.com/z-song/laravel-admin)|后台|快速搭建后台系统|
+|[mews/captcha](https://github.com/mewebstudio/captcha)|验证码|登录注册功能使用验证码验证|
+|[overtrue/laravel-socialite](https://github.com/overtrue/laravel-socialite)|第三方登录|用户登录可以使用Github,QQ,新浪微博|
+|[intervention/image](https://github.com/Intervention/image)|图片处理|是为 Laravel 定制的图片处理工具,加水印|
+|[webpatser/laravel-uuid](https://github.com/webpatser/laravel-uuid)|uuid生成|商品添加增加一个uuid,订单号|
+|[renatomarinho/laravel-page-speed](https://github.com/renatomarinho/laravel-page-speed)|压缩页面DOM|打包优化您的网站自动导致35%以上的优化(已移除使用)|
+|[overtrue/laravel-pinyin](https://github.com/overtrue/laravel-pinyin)|汉语拼音翻译|分类首字母查询|
+|[acelaya/doctrine-enum-type](https://github.com/acelaya/doctrine-enum-type)|枚举|优化代码中的映射|
+
+## Reference
+* [Laravel 的中大型專案架構](https://old-oomusou.goodjack.tw/laravel/architecture/)
+* [十个 Laravel 5 程序优化技巧](https://laravel-china.org/articles/2020/ten-laravel-5-program-optimization-techniques)
+* [十个 Laravel 5 程序优化技巧](http://www.ruanyifeng.com/blog/2018/10/restful-api-best-practices.html)
+* [服务器做了两个优化 CPU 使用率减低 40%(使用缓存优化访问量不写数据库)](https://learnku.com/articles/13366/the-server-has-made-two-optimization-and-the-cpu-utilization-rate-has-been-reduced-by-40)
+
+## Notice
+* 建议开启`bcmath`扩展保证字符串数字运算正确
+* 监听队列如果长时间没反应,或者一直重复任务
+ * 数据库没配置好,导致队列任务表连接不上
+ * 邮件配置出错,导致发送邮件一直失败
+* `composer install`安装不上依赖
+ * 请删除`composer.lock`文件,重新运行`composer install`
+* `SQLSTATE[HY000]: General error: 1215 Cannot add foreign key constraint`
+ * 数据库引擎切换到`InnoDB`
+* `composer install` 安装依赖错误
+ * `composer.lock`锁定了镜像源,删除`composer.lock`再执行即可
+## License
+The MIT License (MIT)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..034e848
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,21 @@
+# Security Policy
+
+## Supported Versions
+
+Use this section to tell people about which versions of your project are
+currently being supported with security updates.
+
+| Version | Supported |
+| ------- | ------------------ |
+| 5.1.x | :white_check_mark: |
+| 5.0.x | :x: |
+| 4.0.x | :white_check_mark: |
+| < 4.0 | :x: |
+
+## Reporting a Vulnerability
+
+Use this section to tell people how to report a vulnerability.
+
+Tell them where to go, how often they can expect to get an update on a
+reported vulnerability, what to expect if the vulnerability is accepted or
+declined, etc.
diff --git a/app/Admin/Actions/Post/ActiveUserAction.php b/app/Admin/Actions/Post/ActiveUserAction.php
new file mode 100644
index 0000000..9ac9534
--- /dev/null
+++ b/app/Admin/Actions/Post/ActiveUserAction.php
@@ -0,0 +1,22 @@
+is_active = 1;
+ $model->save();
+
+ return $this->response()->success('操作成功.')->refresh();
+ }
+
+}
diff --git a/app/Admin/Actions/Post/DividerAction.php b/app/Admin/Actions/Post/DividerAction.php
new file mode 100644
index 0000000..054f834
--- /dev/null
+++ b/app/Admin/Actions/Post/DividerAction.php
@@ -0,0 +1,28 @@
+response()->success('Success message.')->refresh();
+ }
+
+ /**
+ * Render row action.
+ *
+ * @return string
+ */
+ public function render()
+ {
+ return '
';
+ }
+}
diff --git a/app/Admin/Actions/Post/ForceDeleteProductAction.php b/app/Admin/Actions/Post/ForceDeleteProductAction.php
new file mode 100644
index 0000000..259b8cf
--- /dev/null
+++ b/app/Admin/Actions/Post/ForceDeleteProductAction.php
@@ -0,0 +1,30 @@
+forceDelete();
+
+ return $this->response()->success('操作成功.')->refresh();
+ }
+
+
+ public function retrieveModel(Request $request)
+ {
+ if (!$key = $request->get('_key')) {
+ return false;
+ }
+
+ return Product::query()->withTrashed()->findOrFail($key);
+ }
+}
diff --git a/app/Admin/Actions/Post/OrderReceivedAction.php b/app/Admin/Actions/Post/OrderReceivedAction.php
new file mode 100644
index 0000000..72a07e1
--- /dev/null
+++ b/app/Admin/Actions/Post/OrderReceivedAction.php
@@ -0,0 +1,34 @@
+status != OrderStatusEnum::PAID) {
+
+ return back()->withErrors('订单未付款', 'error');
+ }
+
+ if ($order->ship_status != OrderShipStatusEnum::DELIVERED) {
+
+ return back()->withErrors('订单未发货', 'error');
+ }
+
+ $order->ship_status = OrderShipStatusEnum::RECEIVED;
+ $order->save();
+
+ return $this->response()->success('确认收货成功.')->refresh();
+ }
+}
diff --git a/app/Admin/Actions/Post/OrderRefundAction.php b/app/Admin/Actions/Post/OrderRefundAction.php
new file mode 100644
index 0000000..8d43a10
--- /dev/null
+++ b/app/Admin/Actions/Post/OrderRefundAction.php
@@ -0,0 +1,59 @@
+status != OrderStatusEnum::APPLY_REFUND) {
+ return $this->response()->error('订单当前状态禁止退款');
+ }
+
+ $pay = Pay::alipay(config('pay.ali'));
+
+ // 退款数据
+ $refundData = [
+ 'out_trade_no' => $order->no,
+ 'trade_no' => $order->pay_no,
+ 'refund_amount' => $order->pay_amount,
+ 'refund_reason' => '正常退款',
+ ];
+
+ try {
+
+ // 将订单状态改为退款
+ $response = $pay->refund($refundData);
+ $order->pay_refund_fee = $response->get('refund_fee');
+ $order->pay_trade_no = $response->get('trade_no');
+ $order->status = OrderStatusEnum::REFUND;
+ $order->save();
+
+ } catch (\Exception $e) {
+
+ // 调用异常的处理
+ // abort(500, $e->getMessage());
+ return $this->response()->error('服务器异常,请稍后再试');
+ }
+
+ return $this->response()->success('退款成功.')->refresh();
+ }
+
+ public function dialog()
+ {
+ $this->confirm('退款会直接把钱退回到支付账户,是否继续');
+ }
+}
diff --git a/app/Admin/Actions/Post/OrderShipAction.php b/app/Admin/Actions/Post/OrderShipAction.php
new file mode 100644
index 0000000..db820af
--- /dev/null
+++ b/app/Admin/Actions/Post/OrderShipAction.php
@@ -0,0 +1,44 @@
+status != OrderStatusEnum::PAID) {
+
+ return $this->response()->error('订单未付款');
+ }
+
+ $company = $request->input('company');
+ $no = $request->input('no');
+ if (empty($company) || empty($no)) {
+
+ return $this->response()->error('必填项不能为空');
+ }
+
+ $order->ship_status = OrderShipStatusEnum::DELIVERED;
+ $order->express_company = $company;
+ $order->express_no = $no;
+ $order->save();
+
+ return $this->response()->success('发货成功.')->refresh();
+ }
+
+ public function form()
+ {
+ $this->text('company', '物流公司')->required();
+ $this->text('no', '物流单号')->required();
+ }
+}
diff --git a/app/Admin/Actions/Post/ProductStatusAction.php b/app/Admin/Actions/Post/ProductStatusAction.php
new file mode 100644
index 0000000..b739169
--- /dev/null
+++ b/app/Admin/Actions/Post/ProductStatusAction.php
@@ -0,0 +1,46 @@
+name = $name;
+
+ return $this;
+ }
+
+ public function handle(Product $product)
+ {
+ // $model ...
+ // 如果商品已经下架
+ if ($product->trashed()) {
+
+ // 重新上架
+ $product->restore();
+ } else {
+
+ $product->delete();
+ }
+
+
+ return $this->response()->success('操作成功.')->refresh();
+ }
+
+
+ public function retrieveModel(Request $request)
+ {
+ if (!$key = $request->get('_key')) {
+ return false;
+ }
+
+ return Product::query()->withTrashed()->findOrFail($key);
+ }
+}
diff --git a/app/Admin/Controllers/AdminController.php b/app/Admin/Controllers/AdminController.php
new file mode 100644
index 0000000..872da8b
--- /dev/null
+++ b/app/Admin/Controllers/AdminController.php
@@ -0,0 +1,118 @@
+header(trans('admin.administrator'))
+ ->description(trans('admin.list'))
+ ->body($this->adminGrid()->render());
+ }
+
+ protected function adminGrid()
+ {
+ $userModel = config('admin.database.users_model');
+
+ $grid = new Grid(new $userModel());
+
+ $grid->column('id', 'ID')->sortable();
+ $grid->column('username', trans('admin.username'));
+ $grid->column('name', trans('admin.name'));
+ $grid->column('login_ip', '登录ip');
+ $grid->roles(trans('admin.roles'))->pluck('name')->label();
+ $grid->column('created_at', trans('admin.created_at'));
+ $grid->column('updated_at', trans('admin.updated_at'));
+
+ $grid->actions(function (Grid\Displayers\DropdownActions $actions) {
+ if ($actions->getKey() == 1) {
+ $actions->disableDelete();
+ }
+ });
+
+
+ $grid->tools(function (Grid\Tools $tools) {
+ $tools->batch(function (Grid\Tools\BatchActions $actions) {
+ $actions->disableDelete();
+ });
+ });
+
+ return $grid;
+ }
+
+
+ public function indexLogs(Content $content)
+ {
+ return $content
+ ->header(trans('admin.operation_log'))
+ ->description(trans('admin.list'))
+ ->body($this->logGrid());
+ }
+
+ /**
+ * @return Grid
+ */
+ protected function logGrid()
+ {
+ $grid = new Grid(new OperationLog());
+
+ $grid->model()->orderBy('id', 'DESC');
+
+ $grid->column('id', 'ID')->sortable();
+ $grid->column('user.name', '用户');
+ $grid->column('method', '方法')->display(function ($method) {
+ $color = array_get(OperationLog::$methodColors, $method, 'grey');
+
+ return "$method";
+ });
+ $grid->column('path', '路径')->label('info');
+ $grid->column('ip', '地址')->label('primary');
+ $grid->column('description', '描述')->limit(20)->modal(function ($model) {
+
+ return new Box('详情', $model->description ?: ' ');
+ });
+ $grid->column('input', '输入数据')->limit(20)->expand(function ($model) {
+
+ $input = json_decode($model->input, true);
+ $input = array_except($input, ['_pjax', '_token', '_method', '_previous_']);
+ $codes = empty($input) ?
+ '{}
' :
+ ''.json_encode($input, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE).'
';
+
+ return new Box('详情', $codes);
+ });
+
+ $grid->column('created_at', trans('admin.created_at'));
+
+ $grid->actions(function (Grid\Displayers\Actions $actions) {
+ $actions->disableEdit();
+ $actions->disableView();
+ });
+
+ $grid->disableCreateButton();
+
+ $grid->filter(function (Grid\Filter $filter) {
+ $userModel = config('admin.database.users_model');
+
+ $filter->equal('user_id', 'User')->select($userModel::all()->pluck('name', 'id'));
+ $filter->equal('method')->select(array_combine(OperationLog::$methods, OperationLog::$methods));
+ $filter->like('path');
+ $filter->equal('ip');
+ });
+
+ return $grid;
+ }
+}
diff --git a/app/Admin/Controllers/ArticleNotificationController.php b/app/Admin/Controllers/ArticleNotificationController.php
new file mode 100644
index 0000000..3bbcdd5
--- /dev/null
+++ b/app/Admin/Controllers/ArticleNotificationController.php
@@ -0,0 +1,99 @@
+model()->latest();
+
+ $grid->column('id', __('Id'));
+ $grid->column('title', __('Title'));
+ $grid->column('created_at', __('Created at'));
+
+ $grid->filter(function (Grid\Filter $filter) {
+
+ $filter->like('id', __('Id'));
+ $filter->like('title', __('Title'));
+ });
+
+ $grid->actions(function (Grid\Displayers\DropdownActions $actions) {
+
+ $actions->disableEdit();
+ });
+
+ return $grid;
+ }
+
+ /**
+ * Make a show builder.
+ *
+ * @param mixed $id
+ * @return Show
+ */
+ protected function detail($id)
+ {
+ $show = new Show(ArticleNotification::findOrFail($id));
+
+ $show->field('id', __('Id'));
+ $show->field('title', __('Title'));
+ $show->field('content', __('Content'))->unescape();
+ $show->field('created_at', __('Created at'));
+
+ return $show;
+ }
+
+ /**
+ * Make a form builder.
+ *
+ * @return Form
+ */
+ protected function form()
+ {
+ $form = new Form(new ArticleNotification);
+
+ $form->text('title', __('Title'))->help('请再三确认再发布,不可修改');
+ $form->kindeditor('content', __('Content'));
+
+ $form->saving(function (Form $form) {
+
+ if (app()->environment('dev')) {
+
+ admin_toastr('开发环境不允许操作', 'error');
+ return back()->withInput();
+ }
+ });
+
+ $form->deleting(function (Form $form) {
+
+ if (app()->environment('dev')) {
+
+ admin_toastr('开发环境不允许操作', 'error');
+ return back()->withInput();
+ }
+ });
+
+ return $form;
+ }
+}
diff --git a/app/Admin/Controllers/AuthController.php b/app/Admin/Controllers/AuthController.php
new file mode 100644
index 0000000..6ede048
--- /dev/null
+++ b/app/Admin/Controllers/AuthController.php
@@ -0,0 +1,87 @@
+only([$this->username(), 'password']);
+
+ /** @var \Illuminate\Validation\Validator $validator */
+ $validator = Validator::make($credentials, [
+ $this->username() => 'required',
+ 'password' => 'required',
+ ]);
+
+ if ($validator->fails()) {
+ return back()->withInput()->withErrors($validator);
+ }
+
+ if ($this->guard()->attempt($credentials)) {
+
+ // 验证是否登录的 ip
+ $this->authenticated($this->guard()->user());
+
+ // 记录登录日期
+ return $this->sendLoginResponse($request);
+ }
+
+ return back()->withInput()->withErrors([
+ $this->username() => $this->getFailedLoginMessage(),
+ ]);
+ }
+
+ /**
+ * 登录之后提示 ip 地址
+ *
+ * @param Administrator $user
+ */
+ protected function authenticated(Administrator $user)
+ {
+ $ip = request()->getClientIp();
+
+ // 如果两次 ip 不一样, 提示风险
+ if (! is_null($user->login_ip) && $ip != $user->login_ip) {
+
+ admin_info('上一次登录的地址与本次不同,如果不是本人操作,建议及时修改密码');
+ }
+
+ $user->login_ip = $ip;
+ $user->save();
+ }
+
+ public function putSetting()
+ {
+ $form = $this->settingForm();
+
+ $form->submitted(function (Form $form) {
+
+ if (app()->environment('dev')) {
+
+ admin_toastr('开发环境不允许操作', 'error');
+ return back()->withInput();
+ }
+ });
+
+ return $form->update(Admin::user()->id);
+ }
+}
diff --git a/app/Admin/Controllers/CarController.php b/app/Admin/Controllers/CarController.php
new file mode 100644
index 0000000..c8cb307
--- /dev/null
+++ b/app/Admin/Controllers/CarController.php
@@ -0,0 +1,90 @@
+column('id', __('Id'));
+
+ $grid->column('user_id', __('User id'));
+ $grid->column('product_id', __('Product id'));
+
+ $grid->column('user.name', '用户');
+ $grid->column('product.name', '商品');
+ $grid->column('number', __('Number'));
+
+
+ $grid->column('created_at', '收藏时间');
+
+ $grid->disableActions();
+ $grid->disableCreateButton();
+
+ $grid->filter(function (Grid\Filter $filter) {
+
+ $filter->equal('user_id', '用户ID');
+ $filter->equal('product_id', '商品ID');
+ $filter->like('user.name', '用户名');
+ $filter->like('product.name', '商品');
+ });
+
+ return $grid;
+ }
+
+ /**
+ * Make a show builder.
+ *
+ * @param mixed $id
+ * @return Show
+ */
+ protected function detail($id)
+ {
+ $show = new Show(Car::findOrFail($id));
+
+ $show->field('id', __('Id'));
+ $show->field('number', __('Number'));
+ $show->field('product_id', __('Product id'));
+ $show->field('user_id', __('User id'));
+ $show->field('created_at', __('Created at'));
+ $show->field('updated_at', __('Updated at'));
+
+ return $show;
+ }
+
+ /**
+ * Make a form builder.
+ *
+ * @return Form
+ */
+ protected function form()
+ {
+ $form = new Form(new Car);
+
+ $form->number('number', __('Number'))->default(1);
+ $form->number('product_id', __('Product id'));
+ $form->number('user_id', __('User id'));
+
+ return $form;
+ }
+}
diff --git a/app/Admin/Controllers/CategoryController.php b/app/Admin/Controllers/CategoryController.php
new file mode 100644
index 0000000..c2f3d53
--- /dev/null
+++ b/app/Admin/Controllers/CategoryController.php
@@ -0,0 +1,197 @@
+header('商品分类')
+ ->description('')
+ ->row(function (Row $row) {
+
+ // 只能在同一级排序拖动,不允许二级
+ $row->column(6, Category::tree(function (Tree $tree) {
+
+ $tree->disableCreate();
+
+ $tree->nestable(['maxDepth' => 1])
+ ->branch(function ($branch) {
+
+ $icon = "";
+
+ return $icon . ' ' . $branch['title'];
+ });
+ }));
+
+ // 新建表单
+ $row->column(6, function (Column $column) {
+ $form = new \Encore\Admin\Widgets\Form();
+ $form->action(admin_base_path('categories'));
+
+ $form->text('title', '分类名')->rules('required|unique:categories,title');
+ $form->icon('icon', '图标')->default('fa-bars')->rules('required');
+ $form->image('thumb', '缩略图')->uniqueName()->rules('required');
+ $form->hidden('_token')->default(csrf_token());
+
+ $column->append((new Box('新增', $form))->style('success'));
+ });
+ });
+ }
+
+ /**
+ * Show interface.
+ *
+ * @param mixed $id
+ * @param Content $content
+ * @return Content
+ */
+ public function show($id, Content $content)
+ {
+ return $content
+ ->header('详情')
+ ->description('')
+ ->body($this->detail($id));
+ }
+
+ /**
+ * Edit interface.
+ *
+ * @param mixed $id
+ * @param Content $content
+ * @return Content
+ */
+ public function edit($id, Content $content)
+ {
+ return $content
+ ->header('编辑')
+ ->description('')
+ ->body($this->form()->edit($id));
+ }
+
+
+
+ /**
+ * Make a show builder.
+ *
+ * @param mixed $id
+ * @return Show
+ */
+ protected function detail($id)
+ {
+ $show = new Show(Category::findOrFail($id));
+
+ $show->field('id');
+ $show->field('title', '分类名');
+ $show->field('thumb', '缩略图')->unescape()->as(function ($thumb) {
+ return image($thumb);
+ });
+ $show->field('description', '描述');
+ $show->field('order', '排序');
+ $show->field('created_at', '创建时间');
+ $show->field('updated_at', '修改时间');
+
+ return $show;
+ }
+
+ /**
+ * Make a form builder.
+ *
+ * @return Form
+ */
+ protected function form()
+ {
+ $form = new Form(new Category);
+
+ $form->text('title', '分类名');
+ $form->icon('icon', '图标');
+ $form->image('thumb', '缩略图');
+ $form->text('description', '描述');
+
+ $form->saving(function (Form $form) {
+
+ if (app()->environment('dev')) {
+
+ admin_toastr('开发环境不允许操作', 'error');
+ return back()->withInput();
+ }
+ });
+
+
+ return $form;
+ }
+
+
+ /**
+ * 分类下有商品,不允许删除
+ *
+ * @param $id
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function destroy($id)
+ {
+ if (app()->environment('dev')) {
+
+ return response()->json(['status' => false, 'message' => '开发环境不允许操作']);
+ }
+ /**
+ * @var $category Category
+ */
+ $category = Category::query()->findOrFail($id);
+
+ if ($category->products()->exists()) {
+ return response()->json(['status' => false, 'message' => '分类下有商品存在,不允许删除']);
+ }
+
+ if ($category->delete()) {
+ $data = [
+ 'status' => true,
+ 'message' => trans('admin.delete_succeeded'),
+ ];
+ } else {
+ $data = [
+ 'status' => false,
+ 'message' => trans('admin.delete_failed'),
+ ];
+ }
+
+ return response()->json($data);
+ }
+
+ /**
+ * 商品下拉列表
+ *
+ * @param Request $request
+ * @return \Illuminate\Support\Collection
+ */
+ public function getProducts(Request $request)
+ {
+ $id = $request->get('q');
+
+ $category = Category::query()->findOrFail($id);
+
+ // 尽量促销卖得少的商品
+ return $category->products()->orderBy('sale_count', 'desc')->get(['id', 'name as text']);
+ }
+}
diff --git a/app/Admin/Controllers/CommentController.php b/app/Admin/Controllers/CommentController.php
new file mode 100644
index 0000000..25a096d
--- /dev/null
+++ b/app/Admin/Controllers/CommentController.php
@@ -0,0 +1,73 @@
+header('评论列表')
+ ->description('')
+ ->body($this->grid());
+ }
+
+ /**
+ * Make a grid builder.
+ *
+ * @return Grid
+ */
+ protected function grid()
+ {
+ $grid = new Grid(new Comment);
+
+ $grid->model()->latest();
+
+ $grid->column('id');
+ $grid->column('order_id', '订单');
+ $grid->column('product.name', '商品');
+ $grid->column('user.name', '用户');
+ $grid->column('content', '评论内容');
+ $grid->column('score', '评分');
+ $grid->column('created_at', '创建时间');
+ $grid->column('updated_at', '修改时间');
+
+ $grid->filter(function (Grid\Filter $filter) {
+
+ $filter->disableIdFilter();
+ $filter->where(function ($query) {
+
+ $collections = User::query()
+ ->where('name', 'like', "%{$this->input}%")
+ ->pluck('id');
+ $query->whereIn('user_id', $collections->all());
+ }, '用户');
+ $filter->where(function ($query) {
+
+ $collections = Product::query()
+ ->where('name', 'like', "%{$this->input}%")
+ ->pluck('id');
+
+ $query->whereIn('product_id', $collections->all());
+ }, '商品');
+ });
+
+ return $grid;
+ }
+}
diff --git a/app/Admin/Controllers/CouponCodeController.php b/app/Admin/Controllers/CouponCodeController.php
new file mode 100644
index 0000000..80ada3a
--- /dev/null
+++ b/app/Admin/Controllers/CouponCodeController.php
@@ -0,0 +1,190 @@
+model()->latest();
+
+ $grid->column('id', __('Id'));
+ $grid->column('code', '兑换码');
+ $grid->column('user_id', __('User id'));
+ $grid->column('template_id', '优惠券ID');
+
+ $grid->column('user.name', '会员名');
+ $grid->column('template.title', '优惠券名');
+
+ $grid->column('used_at', __('Used at'));
+ $grid->column('notification_at', '上一次发送通知时间');
+ $grid->column('created_at', __('Created at'));
+
+
+ $grid->filter(function (Grid\Filter $filter) {
+
+ $filter->equal('code', '兑换码');
+ $filter->like('user.name', '会员名');
+ $filter->equal('template.title', '优惠券名');
+ $filter->between('used_at')->datetime();
+ });
+
+ $grid->actions(function (Grid\Displayers\Actions $actions) {
+
+ $actions->disableEdit();
+ $actions->disableView();
+ });
+
+ return $grid;
+ }
+
+
+ public function store()
+ {
+ $request = request();
+
+ /**
+ * 发放的优惠券不能为空
+ * @var $template CouponTemplate
+ */
+ $template = CouponTemplate::query()->findOrFail($request->input('template_id'));
+ $today = Carbon::today();
+ if ($today->gt(Carbon::make($template->end_date))) {
+
+ admin_error('优惠券已过期');
+ return back()->withInput();
+ }
+
+ // 如果会员 ID 不为空,则代表是指定会员发放
+ if ($userIds = $request->input('user_ids')) {
+
+ // 使用空格拆分 id
+ $ids = array_values(array_filter(explode(' ', $userIds)));
+ $users = User::query()->findMany($ids);
+ }
+ // 否则则是条件范围发放
+ else {
+
+ $query = User::query();
+ // 性别
+ $sex = array_filter($request->input('user_sex'));
+ // 会员来源
+ $sources = array_filter($request->input('user_source'));
+ // 登录次数
+ $loginCount = (int)$request->input('login_count');
+ // 会员总积分
+ $scoreAll = (int)$request->input('user_score');
+
+ $query->whereIn('sex', $sex)
+ ->whereIn('source', $sources)
+ ->where('login_count', '>=', $loginCount)
+ ->where('score_all', '>=', $scoreAll);
+ $users = $query->get();
+ }
+
+
+ // 开始根据用户发放
+ $now = Carbon::now()->toDateTimeString();
+ $notifications = collect();
+ $codes = $users->map(function (User $user) use ($template, $notifications, $now) {
+
+ $code = strtoupper(str_random(16));
+
+ $notification = [
+ 'id' => Uuid::uuid4()->toString(),
+ 'type' => CouponCodeNotification::class,
+ 'notifiable_id' => $user->id,
+ 'notifiable_type' => get_class($user),
+ 'data' => json_encode((new CouponCodeNotification($template, $code))->toArray($user), JSON_UNESCAPED_UNICODE),
+ 'created_at' => $now,
+ 'updated_at' => $now,
+ ];
+ $notifications->push($notification);
+
+ return [
+ 'code' => $code,
+ 'user_id' => $user->id,
+ 'template_id' => $template->id,
+ 'notification_at' => $now,
+ 'created_at' => $now,
+ 'updated_at' => $now,
+ ];
+ });
+
+
+ $size = 1000;
+ foreach (array_chunk($codes->all(), $size, true) as $chunk) {
+ // 优惠券码
+ CouponCode::query()->insert($codes->all());
+ }
+ foreach (array_chunk($notifications->all(), $size, true) as $chunk) {
+ // 通知
+ DatabaseNotification::query()->insert($chunk);
+ }
+
+ admin_toastr("发布成功,总共有{$users->count()}位会员符合发放条件");
+ return response()->redirectTo(admin_url('/coupon_codes'));
+ }
+
+ /**
+ * Make a form builder.
+ *
+ * @return Form
+ */
+ protected function form()
+ {
+ $form = new Form(new CouponCode);
+
+ $today = Carbon::today()->toDateString();
+
+ $form->divider('指定会员发放');
+ $form->text('user_ids', '会员')->help('请输入会员的ID, 多个会员用空格隔开,如果为空则代表是范围发放');
+
+ $form->divider('范围发放');
+ $templates = CouponTemplate::query()->where('end_date', '>=', $today)->pluck('title', 'id');
+ $form->checkbox('user_sex', '会员性别')->options([UserSexEnum::MAN => '男', UserSexEnum::WOMAN => '女'])->canCheckAll();
+ $form->checkbox('user_source', '会员来源')->options([
+ UserSourceEnum::MOON => '前台注册',
+ UserSourceEnum::GITHUB => 'Github',
+ UserSourceEnum::QQ => 'QQ',
+ UserSourceEnum::WEIBO => '微博',
+ ])->canCheckAll();
+ $form->number('login_count', '会员登录次数')->default(0)->help('会员登录次数大于等于给定的次数');
+ $form->number('user_score', '会员总积分')->default(0)->help('会员给定的积分大于等于总积分');
+
+ $form->divider('优惠券');
+
+ $form->select('template_id', '优惠券')->help('发放的优惠券')->options($templates)->required();
+
+ return $form;
+ }
+}
diff --git a/app/Admin/Controllers/CouponLogController.php b/app/Admin/Controllers/CouponLogController.php
new file mode 100644
index 0000000..de28c64
--- /dev/null
+++ b/app/Admin/Controllers/CouponLogController.php
@@ -0,0 +1,57 @@
+model()->latest();
+
+ $grid->column('id', __('Id'));
+ $grid->column('user_id', '用户 ID');
+ $grid->column('user.name', '用户昵称');
+ $grid->column('title', __('Title'));
+ $grid->column('amount', '满减金额');
+ $grid->column('full_amount', __('Full amount'));
+ $grid->column('start_date', __('Start date'));
+ $grid->column('end_date', __('End date'));
+ $grid->column('is_used', '是否使用')->display(function () {
+
+ return YesNoTransform::trans(! is_null($this->used_at));
+ });
+ $grid->column('used_at', '使用时间');
+ $grid->column('created_at', __('Created at'));
+
+ $grid->disableActions();
+ $grid->disableCreateButton();
+
+ $grid->filter(function (Grid\Filter $filter) {
+
+ $filter->like('title', __('Title'));
+ });
+
+ return $grid;
+ }
+}
diff --git a/app/Admin/Controllers/CouponTemplateController.php b/app/Admin/Controllers/CouponTemplateController.php
new file mode 100644
index 0000000..4bf97b3
--- /dev/null
+++ b/app/Admin/Controllers/CouponTemplateController.php
@@ -0,0 +1,86 @@
+model()->latest('start_date');
+
+ $grid->column('id', __('Id'));
+ $grid->column('title', __('Title'));
+ $grid->column('amount', '优惠金额');
+ $grid->column('full_amount', __('Full amount'));
+ $grid->column('score', '需兑换积分');
+ $grid->column('exp_date', '有效日期')->display(function () {
+
+ return $this->start_date . ' ~ ' . $this->end_date;
+ });
+ $today = Carbon::today();
+ $grid->column('overtime', '是否有效(未过期)')->display(function () use ($today) {
+
+ $endDate = Carbon::make($this->end_date);
+ $startDate = Carbon::make($this->start_date);
+ // 如果没有结束日期,代表永远不过期
+ if (is_null($endDate) || is_null($startDate)) {
+
+ $isOver = true;
+ } else {
+
+ $isOver = $today->gte($startDate) && $today->lte($endDate);
+ }
+
+ return YesNoTransform::trans($isOver);
+ });
+ $grid->column('created_at', __('Created at'));
+
+ $grid->filter(function (Grid\Filter $filter) {
+
+ $filter->like('title', __('Title'));
+ });
+
+ return $grid;
+ }
+
+ /**
+ * Make a form builder.
+ *
+ * @return Form
+ */
+ protected function form()
+ {
+ $form = new Form(new CouponTemplate);
+
+ $form->text('title', __('Title'));
+ $form->decimal('amount', '优惠金额');
+ $form->decimal('full_amount', __('Full amount'));
+ $form->number('score', __('Score'))->default(0)->help('设置为 0 代表无需积分即可兑换优惠券');
+ $form->date('start_date', __('Start date'))->default(date('Y-m-d'));
+ $form->date('end_date', __('End date'))->default(Carbon::today()->addMonth());
+
+ return $form;
+ }
+}
diff --git a/app/Admin/Controllers/HomeController.php b/app/Admin/Controllers/HomeController.php
new file mode 100644
index 0000000..5904f2a
--- /dev/null
+++ b/app/Admin/Controllers/HomeController.php
@@ -0,0 +1,84 @@
+toJson(JSON_UNESCAPED_UNICODE));
+
+ // 使用 echart
+ Admin::js('/js/echarts.min.js');
+
+ return $content
+ ->header('仪表盘')
+ ->row(function (Row $row) use ($service) {
+
+ /**
+ * 今日统计,今天的特殊,需要从缓存 redis 中读取
+ *
+ * @var $todaySite SiteCount
+ */
+ $now = Carbon::now();
+ $today = $now->toDateString();
+ $todaySite = SiteCount::query()->firstOrNew(['date' => $today]);
+ $todaySite = $service->syncByCache($todaySite);
+
+ // 七日统计
+ $lastWeekDate = $now->subDay(7);
+ $weekSites = SiteCount::query()
+ ->where('date', '!=', $today)
+ ->where('date', '>', $lastWeekDate)
+ ->get()
+ ->push($todaySite)
+ ->sortBy('date');
+
+
+ // 本月统计
+ $month = $now->format('Y-m');
+ $monthSites = SiteCount::query()
+ ->where('date', '!=', $today)
+ ->where('date', '>', $month)
+ ->get()
+ ->push($todaySite)
+ ->sortBy('date');
+
+
+ $row->column(4, new Box('今日用户注册来源', new Div('todayRegister')));
+ $row->column(4, new Box('七日用户注册来源', new Div('weekRegister')));
+ $row->column(4, new Box('本月用户注册来源', new Div('monthRegister')));
+
+ $row->column(4, new Box('今日订单', new Div('todayOrders')));
+ $row->column(4, new Box('近期订单量', new Div('weekSites')));
+ $row->column(4, new Box('交易金额', new Div('saleMoney')));
+
+ $allSites = compact('todaySite', 'weekSites', 'monthSites');
+ $row->column(12, view('admin.chars.echart', $allSites));
+ });
+ }
+
+
+ /**
+ * 自定义 404 页面
+ *
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function noFound()
+ {
+ return redirect('admin');
+ }
+}
diff --git a/app/Admin/Controllers/LevelController.php b/app/Admin/Controllers/LevelController.php
new file mode 100644
index 0000000..8bb210b
--- /dev/null
+++ b/app/Admin/Controllers/LevelController.php
@@ -0,0 +1,192 @@
+header('列表')
+ ->description('')
+ ->body($this->grid());
+ }
+
+ /**
+ * Show interface.
+ *
+ * @param mixed $id
+ * @param Content $content
+ * @return Content
+ */
+ public function show($id, Content $content)
+ {
+ return $content
+ ->header('详情')
+ ->description('')
+ ->body($this->detail($id));
+ }
+
+ /**
+ * Edit interface.
+ *
+ * @param mixed $id
+ * @param Content $content
+ * @return Content
+ */
+ public function edit($id, Content $content)
+ {
+ return $content
+ ->header('Edit')
+ ->description('')
+ ->body($this->form()->edit($id));
+ }
+
+ /**
+ * Create interface.
+ *
+ * @param Content $content
+ * @return Content
+ */
+ public function create(Content $content)
+ {
+ return $content
+ ->header('新建')
+ ->description('')
+ ->body($this->form());
+ }
+
+ /**
+ * Make a grid builder.
+ *
+ * @return Grid
+ */
+ protected function grid()
+ {
+ $grid = new Grid(new Level);
+
+ $grid->model()->orderBy('min_score', 'desc');
+
+
+ $grid->column('id', 'id');
+ $grid->column('icon', '图标')->image('', 90, 90);
+ $grid->column('name', '名字')->editable();
+ $grid->column('level', '等级');
+ $grid->column('min_score', '分阶');
+ $grid->column('created_at', '创建时间');
+ $grid->column('updated_at', '更新时间');
+
+
+ $grid->actions(function (Grid\Displayers\Actions $actions) {
+
+ $level = $actions->row;
+ if (! $level->can_delete) {
+ $actions->disableDelete();
+ }
+ });
+
+ return $grid;
+ }
+
+ /**
+ * Make a show builder.
+ *
+ * @param mixed $id
+ * @return Show
+ */
+ protected function detail($id)
+ {
+ $show = new Show(Level::findOrFail($id));
+
+ $show->field('id', 'id');
+ $show->field('icon', '图标')->image();
+ $show->field('name', '名字');
+ $show->field('level', '等级');
+ $show->field('min_score', '分阶');
+ $show->field('created_at', '创建时间');
+ $show->field('updated_at', '更新时间');
+
+ return $show;
+ }
+
+ /**
+ * Make a form builder.
+ *
+ * @return Form
+ */
+ protected function form()
+ {
+ $form = new Form(new Level);
+
+ $icon = $form->image('icon', '图标')->uniqueName();
+ if (! windows_os()) {
+ $icon->resize(160, 160);;
+ }
+
+ $form->text('name', '名字');
+ $form->number('level', '等级');
+ $form->number('min_score', '积分');
+
+ $form->saving(function (Form $form) {
+
+ if (app()->environment('dev')) {
+
+ admin_toastr('开发环境不允许操作', 'error');
+ return back()->withInput();
+ }
+ });
+
+
+ return $form;
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param int $id
+ * @return \Illuminate\Http\Response
+ * @throws \Exception
+ */
+ public function destroy($id)
+ {
+ $level = Level::query()->findOrFail($id);
+
+ if (! $level->can_delete) {
+
+ return response()->json([
+ 'status' => false,
+ 'message' => '这个等级不允许删除',
+ ]);
+ }
+
+ if ($level->delete()) {
+ $data = [
+ 'status' => true,
+ 'message' => trans('admin.delete_succeeded'),
+ ];
+ } else {
+ $data = [
+ 'status' => false,
+ 'message' => trans('admin.delete_failed'),
+ ];
+ }
+
+ return response()->json($data);
+ }
+}
diff --git a/app/Admin/Controllers/OrderController.php b/app/Admin/Controllers/OrderController.php
new file mode 100644
index 0000000..4753203
--- /dev/null
+++ b/app/Admin/Controllers/OrderController.php
@@ -0,0 +1,267 @@
+header('订单列表')
+ ->description('')
+ ->body($this->grid());
+ }
+
+ /**
+ * Show interface.
+ *
+ * @param mixed $id
+ * @param Content $content
+ * @return Content
+ */
+ public function show($id, Content $content)
+ {
+ return $content
+ ->header('详情')
+ ->description('')
+ ->body($this->detail($id));
+ }
+
+
+
+
+ /**
+ * Make a grid builder.
+ *
+ * @return Grid
+ */
+ protected function grid()
+ {
+ $grid = new Grid(new Order);
+
+ $grid->model()->withTrashed()->latest();
+
+ $grid->column('id');
+ $grid->column('no', '流水号');
+ $grid->column('user.name', '用户');
+ $grid->column('origin_amount', '订单原价');
+ $grid->column('post_amount', '邮费');
+ $grid->column('coupon_amount', '优惠');
+ $grid->column('amount', '总价');
+ $grid->column('status', '状态')->display(function ($status) {
+
+ // 如果订单是付款, 那么就修改为物流状态
+ if ($status == OrderStatusEnum::PAID) {
+
+ return OrderShipStatusTransform::trans($this->ship_status);
+ }
+
+ return OrderStatusTransform::trans($status);
+ });
+ $grid->column('type', '订单类型')->display(function ($type) {
+
+ return OrderTypeTransform::trans($type);
+ });
+
+ $grid->column('pay_type', '支付方式')->display(function ($type) {
+
+ return OrderPayTypeTransform::trans($type);
+ });
+ $grid->column('pay_no', '支付流水号');
+ $grid->column('paid_at', '支付时间');
+ $grid->column('consignee_name', '收货人姓名');
+ $grid->column('consignee_phone', '收货人手机');
+ $grid->column('created_at', '创建时间');
+
+ $grid->disableRowSelector();
+ $grid->disableCreateButton();
+ $grid->actions(function (Grid\Displayers\DropdownActions $actions) {
+
+ /**
+ * @var $order Order
+ */
+ $order = $actions->row;
+
+
+ // 如果出现了申请,显示可以退款按钮
+ if ($order->status == OrderStatusEnum::APPLY_REFUND) {
+
+ $actions->add(new OrderRefundAction());
+ } elseif ($order->status == OrderStatusEnum::PAID) {
+
+ if ($order->ship_status == OrderShipStatusEnum::PENDING) {
+
+ $actions->add(new OrderShipAction());
+ } elseif ($order->ship_status == OrderShipStatusEnum::DELIVERED) {
+
+ $actions->add(new OrderReceivedAction());
+ }
+
+ }
+
+ $actions->disableEdit();
+ });
+
+ $grid->filter(function (Filter $filter) {
+
+ $filter->disableIdFilter();
+ $filter->like('no', '流水号');
+ $filter->where(function ($query) {
+
+ $users = User::query()->where('name', 'like', "%{$this->input}%")->pluck('id');
+ $query->whereIn('user_id', $users->all());
+ }, '用户');
+ });
+
+ return $grid;
+ }
+
+ /**
+ * Make a show builder.
+ *
+ * @param mixed $id
+ * @return Show
+ */
+ protected function detail($id)
+ {
+ $show = new Show(Order::query()->withTrashed()->findOrFail($id));
+
+ $show->field('id');
+ $show->field('no', '流水号');
+ $show->field('user', '用户')->as(function ($user) {
+ return optional($user)->name;
+ });
+
+
+ $show->divider();
+
+ $show->field('amount', '总计');
+ $show->field('status', '状态')->as(function ($status) {
+
+ // 如果订单是付款, 那么就修改为物流状态
+ if ($status == OrderStatusEnum::PAID) {
+
+ return OrderShipStatusTransform::trans($this->ship_status);
+ }
+
+ return OrderStatusTransform::trans($status);
+ });
+ $show->field('type', '订单类型')->as(function ($type) {
+
+ return OrderTypeTransform::trans($type);
+ });
+
+ $show->divider();
+
+ $show->field('express_company', '物流公司');
+ $show->field('express_no', '物流单号');
+
+ $show->divider();
+
+ $show->field('consignee_name', '收货人');
+ $show->field('consignee_phone', '收货人手机');
+ $show->field('consignee_address', '收货地址');
+
+ $show->divider();
+
+ $show->field('pay_type', '支付类型')->as(function ($type) {
+
+ return OrderPayTypeTransform::trans($type);
+ });
+ $show->field('refund_reason', '退款理由');
+ $show->field('pay_trade_no', '退款单号');
+ $show->field('pay_no', '支付单号');
+ $show->field('paid_at', '支付时间');
+ $show->field('created_at', '创建时间');
+ $show->field('updated_at', '修改时间');
+
+ // 详情
+ $show->details('详情', function (Grid $details) {
+
+ $details->column('id');
+ $details->column('product.name', '商品名字');
+ $details->column('price', '单价');
+ $details->column('number', '数量');
+ $details->column('is_commented', '是否评论')->display(function ($is) {
+
+ return YesNoTransform::trans($is);
+ });
+ $details->column('total', '小计');
+
+ $details->disableRowSelector();
+ $details->disableCreateButton();
+ $details->disableFilter();
+ $details->disableActions();
+ });
+
+ return $show;
+ }
+
+ /**
+ * 后台删除订单就是真的删除了
+ * @param $id
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function destroy($id)
+ {
+ try {
+
+ DB::transaction(function () use ($id) {
+ /**
+ * @var $order Order
+ */
+ $order = Order::withTrashed()->findOrFail($id);
+ $order->details()->delete();
+ $order->forceDelete();
+ });
+
+ $data = [
+ 'status' => true,
+ 'message' => trans('admin.delete_succeeded'),
+ ];
+ } catch (\Throwable $e) {
+ $data = [
+ 'status' => false,
+ 'message' => trans('admin.delete_failed') . $e->getMessage(),
+ ];
+ }
+
+ return response()->json($data);
+ }
+}
diff --git a/app/Admin/Controllers/ProductController.php b/app/Admin/Controllers/ProductController.php
new file mode 100644
index 0000000..3eaa888
--- /dev/null
+++ b/app/Admin/Controllers/ProductController.php
@@ -0,0 +1,249 @@
+header('商品列表')
+ ->description('')
+ ->body($this->grid());
+ }
+
+ /**
+ * Show interface.
+ *
+ * @param mixed $id
+ * @param Content $content
+ * @return Content
+ */
+ public function show($id, Content $content)
+ {
+ return $content
+ ->header('详情')
+ ->description('')
+ ->body($this->detail($id));
+ }
+
+ /**
+ * Edit interface.
+ *
+ * @param mixed $id
+ * @param Content $content
+ * @return Content
+ */
+ public function edit($id, Content $content)
+ {
+ return $content
+ ->header('编辑')
+ ->description('')
+ ->body($this->form()->edit($id));
+ }
+
+ /**
+ * Create interface.
+ *
+ * @param Content $content
+ * @return Content
+ */
+ public function create(Content $content)
+ {
+ return $content
+ ->header('添加商品')
+ ->description('为你的商城添加一个商品')
+ ->body($this->form());
+ }
+
+ /**
+ * Make a grid builder.
+ *
+ * @return Grid
+ */
+ protected function grid()
+ {
+ $grid = new Grid(new Product);
+
+ $grid->model()->withTrashed()->latest();
+
+ $grid->column('id');
+ $grid->column('category.title', '商品类别');
+ $grid->column('name', '商品名')->display(function ($name) {
+ return str_limit($name, 30);
+ });
+ $grid->column('thumb', '首图')->image('', 50, 50);
+ $grid->column('price', '价格')->display(function ($price) {
+
+ return $price . '/' . $this->original_price;
+ });
+ $grid->column('view_count', '浏览次数')->sortable();
+ $grid->column('sale_count', '售出数量')->sortable();
+ $grid->column('count', '库存量')->sortable();
+ $grid->column('deleted_at', '是否上架')->display(function ($isAlive) {
+
+ return YesNoTransform::trans(is_null($isAlive));
+ });
+ $grid->column('created_at', '创建时间');
+ $grid->column('updated_at', '修改时间');
+
+
+ // 查询过滤
+ $grid->filter(function (Grid\Filter $filter) {
+
+ $categories = Category::query()
+ ->orderBy('order')
+ ->latest()
+ ->pluck('title', 'id')
+ ->all();
+
+ $filter->disableIdFilter();
+ $filter->equal('category_id', '分类')->select($categories);
+ $filter->equal('id', 'ID');
+ $filter->equal('uuid', 'UUID');
+ $filter->like('name', '商品名字');
+ });
+
+
+ // 增加一个上架,下架功能
+ $grid->actions(function (Grid\Displayers\DropdownActions $actions) {
+
+
+ $actions->disableDelete();
+ $actions->add(new ForceDeleteProductAction());
+ $actions->add(new DividerAction());
+
+ $name = is_null($actions->row->deleted_at) ?
+ "下架":
+ "上架";
+
+ $statusAction = new ProductStatusAction();
+ $statusAction->setName($name);
+
+ $actions->add($statusAction);
+ });
+
+ return $grid;
+ }
+
+ /**
+ * Make a show builder.
+ *
+ * @param mixed $id
+ * @return Show
+ */
+ protected function detail($id)
+ {
+ $show = new Show(Product::query()->withTrashed()->findOrFail($id));
+
+ $show->field('id');
+ $show->field('category.title', '商品类别');
+ $show->field('name', '商品名');
+ $show->field('title', '卖点');
+ $show->field('thumb', '缩略图')->image();
+ $show->field('price', '价格')->as(function ($price) {
+ return $price . '/' . $this->original_price;
+ });
+ $show->field('view_count', '浏览次数');
+ $show->field('sale_count', '售出数量');
+ $show->field('count', '库存量');
+ $show->field('deleted_at', '是否上架')->as(function ($isAlive) {
+ return is_null($isAlive) ? '上架' : '下架';
+ });
+ $show->field('created_at', '创建时间');
+ $show->field('updated_at', '修改时间');
+
+
+ return $show;
+ }
+
+ /**
+ * Make a form builder.
+ *
+ * @return Form
+ */
+ protected function form()
+ {
+ $form = new Form(new Product);
+
+
+ $form->select('category_id', '类别')->options(Category::selectOrderAll())->rules('required|exists:categories,id');
+ $form->text('name', '商品名字')->rules(function (Form $form) {
+
+ $rules = 'required|max:50|unique:products,name';
+ if ($id = $form->model()->id) {
+ $rules .= ',' . $id;
+ }
+
+ return $rules;
+ });
+ $form->textarea('title', '卖点')->rules('required|max:199');
+ $form->currency('price', '销售价')->symbol('$')->rules('required|numeric');
+ $form->currency('original_price', '原价')->symbol('$')->rules('required|numeric');
+ $form->number('count', '库存量')->rules('required|integer|min:0');
+
+ $form->image('thumb', '缩略图')->uniqueName()->move('products/thumb')->rules('required');
+ $form->multipleImage('pictures', '轮播图')->uniqueName()->move('products/lists');
+
+ $form->editor('detail.content', '详情')->rules('required');
+
+ $form->saving(function (Form $form) {
+
+ if (app()->environment('dev')) {
+
+ admin_toastr('开发环境不允许操作', 'error');
+ return back()->withInput();
+ }
+ });
+
+ return $form;
+ }
+
+
+ public function destroy($id)
+ {
+ $product = Product::query()->withTrashed()->findOrFail($id);
+
+ if (app()->environment('dev')) {
+
+ admin_toastr('开发环境不允许操作', 'error');
+ return back()->withInput();
+ }
+
+ if ($product->forceDelete()) {
+ $data = [
+ 'status' => true,
+ 'message' => trans('admin.delete_succeeded'),
+ ];
+ } else {
+ $data = [
+ 'status' => false,
+ 'message' => trans('admin.delete_failed'),
+ ];
+ }
+
+ return response()->json($data);
+ }
+}
diff --git a/app/Admin/Controllers/ProductLikeController.php b/app/Admin/Controllers/ProductLikeController.php
new file mode 100644
index 0000000..bacb031
--- /dev/null
+++ b/app/Admin/Controllers/ProductLikeController.php
@@ -0,0 +1,59 @@
+model()->latest();
+
+ $grid->column('user_id', __('User id'));
+ $grid->column('product_id', __('Product id'));
+
+ $grid->column('user.name', '用户');
+ $grid->column('created_at', '收藏时间');
+ $grid->column('product.name', '商品');
+ $grid->column('product.price', '价格')->display(function ($price) {
+
+ return $price . '/' . $this->product['original_price'];
+ });
+ $grid->column('product.thumb', '首图')->image('', 50, 50);
+
+
+ $grid->disableActions();
+ $grid->disableCreateButton();
+ $grid->disableBatchActions();
+
+ $grid->filter(function (Grid\Filter $filter) {
+
+ $filter->disableIdFilter();
+ $filter->like('user_id', '用户ID');
+ $filter->like('product_id', '商品ID');
+ $filter->like('user.name', '用户名');
+ $filter->equal('product.name', '商品');
+ });
+
+ return $grid;
+ }
+}
diff --git a/app/Admin/Controllers/ScoreLogController.php b/app/Admin/Controllers/ScoreLogController.php
new file mode 100644
index 0000000..5b46229
--- /dev/null
+++ b/app/Admin/Controllers/ScoreLogController.php
@@ -0,0 +1,50 @@
+model()->latest();
+
+ $grid->column('id', __('Id'));
+ $grid->column('user_id', __('User id'));
+ $grid->column('user.name', '用户名');
+ $grid->column('description', __('Description'));
+ $grid->column('score', __('Score'));
+ $grid->column('created_at', __('Created at'));
+ $grid->column('updated_at', __('Updated at'));
+
+ $grid->disableActions();
+ $grid->disableCreateButton();
+
+ $grid->filter(function (Grid\Filter $filter) {
+
+ $filter->like('user.name', '用户名');
+ $filter->equal('score', '积分');
+ });
+
+ return $grid;
+ }
+}
diff --git a/app/Admin/Controllers/ScoreRuleController.php b/app/Admin/Controllers/ScoreRuleController.php
new file mode 100644
index 0000000..beb2f6c
--- /dev/null
+++ b/app/Admin/Controllers/ScoreRuleController.php
@@ -0,0 +1,227 @@
+header('列表')
+ ->description(':xxx 是变量模板,建议不要操作')
+ ->body($this->grid());
+ }
+
+ /**
+ * Show interface.
+ *
+ * @param mixed $id
+ * @param Content $content
+ * @return Content
+ */
+ public function show($id, Content $content)
+ {
+ return $content
+ ->header('详情')
+ ->description('')
+ ->body($this->detail($id));
+ }
+
+ /**
+ * Edit interface.
+ *
+ * @param mixed $id
+ * @param Content $content
+ * @return Content
+ */
+ public function edit($id, Content $content)
+ {
+ return $content
+ ->header('Edit')
+ ->description('')
+ ->body($this->form($id)->edit($id));
+ }
+
+ /**
+ * Create interface.
+ *
+ * @param Content $content
+ * @return Content
+ */
+ public function create(Content $content)
+ {
+ return $content
+ ->header('新建')
+ ->description('')
+ ->body($this->form());
+ }
+
+ /**
+ * Make a grid builder.
+ *
+ * @return Grid
+ */
+ protected function grid()
+ {
+ $grid = new Grid(new ScoreRule);
+
+ $grid->model()->latest();
+
+ $grid->column('id', 'id');
+ $grid->column('description', '描述');
+ $grid->column('replace_text', '替换文本');
+ $grid->column('score', '积分');
+ $grid->column('times', '次数')->display(function ($times) {
+
+ return $times ? $times : '';
+ });
+
+ $grid->column('created_at', '创建时间');
+ $grid->column('updated_at', '修改时间');
+
+ $grid->actions(function (Grid\Displayers\Actions $actions) {
+
+ $rule = $actions->row;
+ if (! $rule->can_delete) {
+ $actions->disableDelete();
+ }
+
+ });
+
+ return $grid;
+ }
+
+ /**
+ * Make a show builder.
+ *
+ * @param mixed $id
+ * @return Show
+ */
+ protected function detail($id)
+ {
+ $show = new Show(ScoreRule::findOrFail($id));
+
+ $show->field('id', 'id');
+ $show->field('description', '描述');
+ $show->field('replace_text', '替换文本');
+ $show->field('score', '积分');
+ $show->field('times', '次数')->as(function ($times) {
+
+ return $times ? $times : '';
+ });
+ $show->field('created_at', '创建时间');
+ $show->field('updated_at', '修改时间');
+
+ return $show;
+ }
+
+ /**
+ * Make a form builder.
+ *
+ * @param null $id
+ * @return Form
+ */
+ protected function form($id = null)
+ {
+ $form = new Form(new ScoreRule);
+
+ // 只有新增才能改变类型
+ $options = [ScoreRuleIndexEnum::CONTINUE_LOGIN => '连续登录', ScoreRuleIndexEnum::VISITED_PRODUCT => '每日浏览商品'];
+
+ if (is_null($id)) {
+
+ $form->select('index_code', '类型')->options($options)->rules('required');
+ }
+ $form->number('score', '积分');
+
+
+ if (! is_null($id)) {
+
+ // 只有当时连续登录和修改的才有次数
+ $scoreRule = ScoreRule::query()->findOrFail($id);
+ $form->textarea('replace_text', '替换文本');
+ $form->textarea('description', '描述');
+
+ if (array_key_exists($scoreRule->index_code, $options)) {
+
+ $form->number('times', '次数');
+ }
+
+ } else {
+
+ $form->number('times', '次数');
+ }
+
+
+ $form->saving(function (Form $form) {
+
+ // 如果是新建复制模板
+ if (! $form->model()->exists) {
+
+
+ $rule = ScoreRule::query()
+ ->where('can_delete', 0)
+ ->where('index_code', $form->index_code)
+ ->firstOrFail();
+
+ $form->model()->description = $rule->description;
+ $form->model()->replace_text = $rule->replace_text;
+ }
+
+ });
+
+ return $form;
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param int $id
+ * @return \Illuminate\Http\Response
+ * @throws \Exception
+ */
+ public function destroy($id)
+ {
+ $rule = ScoreRule::query()->findOrFail($id);
+
+ if (! $rule->can_delete) {
+
+ return response()->json([
+ 'status' => false,
+ 'message' => '这个等级不允许删除',
+ ]);
+ }
+
+ if ($rule->delete()) {
+ $data = [
+ 'status' => true,
+ 'message' => trans('admin.delete_succeeded'),
+ ];
+ } else {
+ $data = [
+ 'status' => false,
+ 'message' => trans('admin.delete_failed'),
+ ];
+ }
+
+ return response()->json($data);
+ }
+}
diff --git a/app/Admin/Controllers/SeckillController.php b/app/Admin/Controllers/SeckillController.php
new file mode 100644
index 0000000..bc60c64
--- /dev/null
+++ b/app/Admin/Controllers/SeckillController.php
@@ -0,0 +1,222 @@
+header('秒杀列表')
+ ->description('')
+ ->body($this->grid());
+ }
+
+ /**
+ * Create interface.
+ *
+ * @param Content $content
+ * @return Content
+ */
+ public function create(Content $content)
+ {
+ return $content
+ ->header('新建秒杀')
+ ->description('')
+ ->body($this->form());
+ }
+
+ /**
+ * Make a grid builder.
+ *
+ * @return Grid
+ */
+ protected function grid()
+ {
+ $grid = new Grid(new Seckill);
+
+ $grid->model()->latest();
+
+ $grid->column('id');
+
+ $grid->column('product.id', '商品ID');
+ $grid->column('product.name', '商品名字')->display(function ($name) {
+ return str_limit($name, 30);
+ });
+ $grid->column('product.thumb', '商品图')->display(function ($thumb) {
+ return image($thumb);
+ });
+
+ $grid->column('price', '秒杀价');
+ $grid->column('number', '秒杀数量');
+ $grid->column('start_at', '开始时间');
+ $grid->column('end_at', '结束时间');
+ $grid->column('rollback_count', '回滚量');
+ $grid->column('is_rollback', '是否回滚数量')->display(function ($is) {
+ return $is ? '是' : '否';
+ });
+ $grid->column('created_at', '创建时间');
+ $grid->column('updated_at', '修改时间');
+
+ $grid->actions(function (Grid\Displayers\Actions $actions) {
+
+ $actions->disableView();
+ $actions->disableEdit();
+ });
+
+ return $grid;
+ }
+
+
+
+ /**
+ * Make a form builder.
+ *
+ * @return Form
+ */
+ protected function form()
+ {
+ $form = new Form(new Seckill);
+
+ $categories = Category::selectOrderAll();
+ $form->select('category_id', '分类')
+ ->options($categories)
+ ->load('product_id', admin_url('api/products'));
+ $form->select('product_id', '秒杀商品');
+
+ $form->number('price', '秒杀价')
+ ->default(1);
+ $form->number('number', '秒杀数量')
+ ->default(1)
+ ->help('保证商品的库存数量大于此数量,会从库存中减去');
+
+ $now = Carbon::now();
+ $form->datetime('start_at', '开始时间')
+ ->default($now->format('Y-m-d H:00:00'));
+ $form->datetime('end_at', '结束时间')
+ ->default($now->addHour(2)->format('Y-m-d H:00:00'))
+ ->rules('required|date|after_or_equal:start_at');
+
+ return $form;
+ }
+
+ /**
+ * @param Request $request
+ * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ * @throws \Exception
+ */
+ public function store(Request $request)
+ {
+ $number = $request->input('number', 0);
+ $product = Product::query()->findOrFail($request->input('product_id'));
+
+ if ($number > $product->count) {
+
+ return back()->withInput()->withErrors(['number' => '秒杀数量不能大于库存数量']);
+ }
+
+ DB::beginTransaction();
+
+ try {
+
+ $attributes = $request->only(['category_id', 'product_id', 'price', 'number', 'start_at', 'end_at']);
+ Seckill::create($attributes);
+
+ // 减去库存数量
+ $product->decrement('count', $number);
+
+ } catch (\Error $e) {
+
+ DB::rollBack();
+ return back()->withInput()->withErrors(['category_id' => $e->getMessage()]);
+ }
+
+ DB::commit();
+
+ admin_toastr('添加成功');
+ return back();
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param int $id
+ * @return \Illuminate\Http\Response
+ * @throws \Exception
+ */
+ public function destroy($id)
+ {
+ /**
+ * @var $seckill Seckill
+ * @var $product Product
+ */
+ $seckill = Seckill::query()->findOrFail($id);
+
+
+ // 如果已经到了开始抢购时间,就不能删除了
+ $now = Carbon::now();
+ $startTime = Carbon::make($seckill->start_at);
+ $endTime = Carbon::make($seckill->end_at);
+
+ // 如果正处于抢购的时间,不允许删除
+ if ($now->gte($startTime) && $now->lte($endTime)) {
+
+ return response()->json([
+ 'status' => false,
+ 'message' => '秒杀已经开始,不能删除',
+ ]);
+ }
+
+ DB::beginTransaction();
+
+
+ try {
+
+ // 先把 redis 数据删除掉
+ // 虽然过期会自动清理,但是如果用户是
+ // 删除还没有开始的秒杀,只能在这里手动清理
+ \Redis::connection()->del([$seckill->getRedisModelKey(), $seckill->getRedisQueueKey()]);
+
+
+ $seckill->delete();
+
+ $data = [
+ 'status' => false,
+ 'message' => trans('admin.delete_succeeded'),
+ ];
+
+ } catch (\Exception $e) {
+
+ $data = [
+ 'status' => true,
+ 'message' => trans('admin.delete_failed'),
+ ];
+ DB::rollBack();
+ }
+
+ DB::commit();
+ return response()->json($data);
+ }
+}
diff --git a/app/Admin/Controllers/SettingController.php b/app/Admin/Controllers/SettingController.php
new file mode 100644
index 0000000..b9ec991
--- /dev/null
+++ b/app/Admin/Controllers/SettingController.php
@@ -0,0 +1,99 @@
+header('配置列表')
+ ->description('')
+ ->body(function (Row $row) {
+
+
+ $settingKeys = $this->getSettingKeys();
+
+ $settings = [];
+ foreach ($settingKeys as $key) {
+
+ $settings[$key] = setting(new SettingKeyEnum($key));
+ }
+
+ $form = new \Encore\Admin\Widgets\Form($settings);
+ $form->action(admin_url('settings'));
+ $form->method('POST');
+ $form->hidden('_token', csrf_token());
+
+
+ $form->text(SettingKeyEnum::USER_INIT_PASSWORD, '会员初始密码')->required();
+ $form->decimal(SettingKeyEnum::UN_PAY_CANCEL_TIME, '订单自动取消时间')->required()->help('单位:分钟');
+ $form->decimal(SettingKeyEnum::POST_AMOUNT, '邮费')->required()->help('设置为 0 免邮');
+
+ $states = [
+ 'on' => ['value' => 1, 'text' => '打开', 'color' => 'success'],
+ 'off' => ['value' => 0, 'text' => '关闭', 'color' => 'danger'],
+ ];
+
+ $seckillStatus = \setting(new SettingKeyEnum(SettingKeyEnum::IS_OPEN_SECKILL)) ? '开启' : '关闭';
+ $seckillHelp = "秒杀现在是 {$seckillStatus} 状态";
+ $form->switch(SettingKeyEnum::IS_OPEN_SECKILL, '是否开启秒杀')->states($states)->help($seckillHelp);
+
+ $row->column(12, new Box('网站配置', $form));
+ });
+ }
+
+ public function store(Request $request)
+ {
+ // 对秒杀这个特殊的 key 做处理
+ $val = strtolower($request->input(SettingKeyEnum::IS_OPEN_SECKILL)) == 'on' ? 1 : 0;
+ $request->offsetSet(SettingKeyEnum::IS_OPEN_SECKILL, $val);
+
+ $this->validate($request, [
+ SettingKeyEnum::USER_INIT_PASSWORD => 'required|string',
+ SettingKeyEnum::UN_PAY_CANCEL_TIME => 'required|integer|min:0',
+ SettingKeyEnum::IS_OPEN_SECKILL => 'required|int|in:0,1',
+ SettingKeyEnum::POST_AMOUNT => 'required|numeric|min:0',
+ ]);
+
+
+ $settingKeys = $this->getSettingKeys();
+ foreach ($settingKeys as $key) {
+
+ Setting::query()->where('key', $key)->update(['value' => $request->input($key)]);
+ }
+
+ admin_success('修改成功');
+ return back();
+ }
+
+ private function getSettingKeys()
+ {
+ return [
+ SettingKeyEnum::USER_INIT_PASSWORD,
+ SettingKeyEnum::UN_PAY_CANCEL_TIME,
+ SettingKeyEnum::POST_AMOUNT,
+ SettingKeyEnum::IS_OPEN_SECKILL,
+ ];
+ }
+}
diff --git a/app/Admin/Controllers/UploadController.php b/app/Admin/Controllers/UploadController.php
new file mode 100644
index 0000000..3087669
--- /dev/null
+++ b/app/Admin/Controllers/UploadController.php
@@ -0,0 +1,39 @@
+setFileInput('pictures')
+ ->setMaxSize('10M')
+ ->setExtensions(['jpg', 'jpeg', 'png', 'bmp', 'gif'])
+ ->validate()
+ ->storeMulti('upload/editor', compact('disk'));
+
+ $files = collect($files)->map(function ($file) use ($disk) {
+ return Storage::disk($disk)->url($file);
+ })->all();
+
+
+ } catch (UploadException $e) {
+
+ return ['errno' => 1, 'msg' => $e->getMessage()];
+ }
+
+ return ['errno' => 0, 'data' => $files];
+ }
+}
diff --git a/app/Admin/Controllers/UserController.php b/app/Admin/Controllers/UserController.php
new file mode 100644
index 0000000..a99a70c
--- /dev/null
+++ b/app/Admin/Controllers/UserController.php
@@ -0,0 +1,285 @@
+header('会员列表')
+ ->description('')
+ ->body($this->grid());
+ }
+
+ /**
+ * Show interface.
+ *
+ * @param mixed $id
+ * @param Content $content
+ * @return Content
+ */
+ public function show($id, Content $content)
+ {
+ return $content
+ ->header('详情')
+ ->description('')
+ ->body($this->detail($id));
+ }
+
+ /**
+ * Edit interface.
+ *
+ * @param mixed $id
+ * @param Content $content
+ * @return Content
+ */
+ public function edit($id, Content $content)
+ {
+ return $content
+ ->header('Edit')
+ ->description('')
+ ->body($this->form()->edit($id));
+ }
+
+ /**
+ * Create interface.
+ *
+ * @param Content $content
+ * @return Content
+ */
+ public function create(Content $content)
+ {
+ return $content
+ ->header('新建')
+ ->description('')
+ ->body($this->form());
+ }
+
+ /**
+ * Make a grid builder.
+ *
+ * @return Grid
+ */
+ protected function grid()
+ {
+ $grid = new Grid(new User);
+
+ $levels = Level::query()->orderBy('min_score', 'desc')->get();
+
+ // 排序最新的
+ $grid->model()->latest();
+
+ $grid->column('id', 'Id');
+ $grid->column('name', '用户名');
+ $grid->column('sex', '性别')->display(function ($sex) {
+
+ return UserSexTransform::trans($sex);
+ });
+ $grid->column('email', '邮箱')->display(function ($email) {
+ return str_limit($email, 20);
+ });
+ $grid->column('avatar', '头像')->image('', 50, 50);
+ $grid->column('github_name', 'Github');
+ $grid->column('qq_name', 'QQ');
+ $grid->column('weibo_name', '微博');
+ $grid->column('level', '等级')->display(function () use ($levels) {
+
+ $level = $levels->where('min_score', '<=', $this->score_all)->first();
+ return optional($level)->name;
+ });
+ $grid->column('score_all', '总积分')->sortable();
+ $grid->column('score_now', '剩余积分')->sortable();
+ $grid->column('login_ip', '登录地址');
+ $grid->column('login_count', '登录次数')->sortable();
+ $grid->column('is_active', '是否激活')->display(function ($isActive) {
+
+ return YesNoTransform::trans($isActive);
+ });
+ $grid->column('created_at', '创建时间');
+ $grid->column('updated_at', '修改时间');
+
+ $grid->actions(function (Grid\Displayers\DropdownActions $actions) {
+
+ if (! $actions->row->is_active) {
+
+ $actions->add(new ActiveUserAction());
+ }
+ });
+
+ // 筛选功能
+ $levelOptions = $levels->pluck('name', 'id');
+ $grid->filter(function (Filter $filter) use ($levelOptions) {
+ $filter->disableIdFilter();
+ $filter->like('name', '用户名');
+ $filter->like('email', '邮箱');
+
+ $filter->where(function ($query) {
+
+ // 找到这个等级
+ $level = Level::query()->findOrFail($this->input);
+ // 找到下一个等级
+ $high = Level::query()->where('min_score', '>', $level->min_score)->orderBy('min_score', 'asc')->first();
+
+
+ $query->where('score_all', '>=', $level->min_score);
+ if (! is_null($high)) {
+ $query->where('score_all', '<', $high->min_score);
+ }
+
+ }, '等级')->select($levelOptions);
+ });
+
+ return $grid;
+ }
+
+ /**
+ * Make a show builder.
+ *
+ * @param mixed $id
+ * @return Show
+ */
+ protected function detail($id)
+ {
+ $show = new Show(User::findOrFail($id));
+
+
+ $show->field('id', 'Id');
+ $show->field('name', '用户名');
+ $show->field('sex', '性别')->as(function ($sex) {
+
+ return UserSexTransform::trans($sex);
+ });
+ $show->field('email', '邮箱');
+ $show->field('avatar', '头像')->image();
+ $show->field('github_name', 'Github昵称');
+ $show->field('qq_name', 'QQ昵称');
+ $show->field('weibo_name', '微博昵称');
+ $show->field('login_ip', '登录地址');
+ $show->field('login_count', '登录次数');
+ $show->field('is_active', '是否激活')->as(function ($isActive) {
+
+ return YesNoTransform::trans($isActive);
+ })->unescape();
+ $show->field('created_at', '创建时间');
+ $show->field('updated_at', '修改时间');
+
+ $show->addresses('收货地址', function (Grid $grid) {
+
+ $grid->model()->latest();
+ $grid->column('name', '收货人');
+ $grid->column('phone', '收货人联系方式');
+ $grid->column('detail_address', '详细地址');
+ $grid->column('is_default', '是否默认')->display(function ($is) {
+
+ return YesNoTransform::trans($is);
+ });
+ $grid->column('created_at', '创建时间');
+
+ $grid->disableActions();
+ $grid->disableCreateButton();
+ $grid->disableFilter();
+ $grid->disableTools();
+ $grid->disableRowSelector();
+ });
+
+ $show->scoreLogs('积分', function (Grid $grid) {
+
+ $grid->model()->latest();
+ $grid->column('description', '描述');
+ $grid->column('score', '积分');
+ $grid->column('created_at', '创建时间');
+
+ $grid->disableActions();
+ $grid->disableCreateButton();
+ $grid->disableFilter();
+ $grid->disableTools();
+ $grid->disableRowSelector();
+ });
+
+ return $show;
+ }
+
+ /**
+ * Make a form builder.
+ *
+ * @return Form
+ */
+ protected function form()
+ {
+ // 前台用户注册必须要有这个 token,兼容一下
+ $form = new Form(tap(new User, function ($user) {
+ $user->active_token = str_random(60);
+ }));
+
+ $form->text('name', '用户名')->rules(function (Form $form) {
+ $rules = 'required|unique:users,id';
+
+ // 更新操作
+ if (! is_null($id = $form->model()->getKey())) {
+ $rules .= ",{$id}";
+ }
+
+ return $rules;
+ });
+
+ $sexOptions = [UserSexEnum::MAN => '男', UserSexEnum::WOMAN => '女'];
+ $form->select('sex', '性别')->rules(['required', Rule::in(array_keys($sexOptions))])->options($sexOptions)->default(1);
+ $form->email('email', '邮箱')->rules(function (Form $form) {
+ $rules = 'required|email|unique:users,email';
+
+ // 更新操作
+ if (! is_null($id = $form->model()->getKey())) {
+ $rules .= ",{$id}";
+ }
+
+ return $rules;
+ });
+ // dd(windows_os());
+ $form->password('password', '密码');
+ $avatar = $form->image('avatar', '头像')->uniqueName()->move('avatars');
+
+ if (! windows_os()) {
+ $avatar->resize(160, 160);;
+ }
+
+ $form->switch('is_active', '激活');
+
+ // 加密密码
+ $form->saving(function (Form $form) {
+
+ if ($form->password) {
+ $form->password = bcrypt($form->password);
+ } else {
+ $form->password = $form->model()->password;
+ }
+
+ });
+
+ return $form;
+ }
+}
diff --git a/app/Admin/Extensions/Div.php b/app/Admin/Extensions/Div.php
new file mode 100644
index 0000000..1841d52
--- /dev/null
+++ b/app/Admin/Extensions/Div.php
@@ -0,0 +1,44 @@
+id = $id;
+
+ if (! is_null($width)) {
+ $this->width = $width;
+ }
+
+ if (! is_null($height)) {
+ $this->height = $height;
+ }
+ }
+
+
+ public function render()
+ {
+ return <<
+div;
+
+ }
+}
diff --git a/app/Admin/Extensions/WangEditor.php b/app/Admin/Extensions/WangEditor.php
new file mode 100644
index 0000000..18a99a4
--- /dev/null
+++ b/app/Admin/Extensions/WangEditor.php
@@ -0,0 +1,56 @@
+formatName($this->column);
+ $token = csrf_token();
+ $url = admin_base_path('upload/editor');
+
+ $this->script = <<id}');
+editor.customConfig.zIndex = 0
+editor.customConfig.uploadFileName = 'pictures[]'
+// 配置服务器端地址
+editor.customConfig.uploadImgServer = '{$url}'
+editor.customConfig.uploadImgParams = {
+ _token: '{$token}'
+}
+// 文件改变添加内容到隐藏域
+editor.customConfig.onchange = function (html) {
+ $('input[name=\'$name\']').val(html);
+}
+// 监听上传错误
+editor.customConfig.uploadImgHooks = {
+ fail: function (xhr, editor) {
+ var response = $.parseJSON(xhr.response);
+
+ alert(response.msg);
+ }
+}
+editor.create()
+
+EOT;
+ return parent::render();
+ }
+}
diff --git a/app/Admin/Transforms/OrderPayTypeTransform.php b/app/Admin/Transforms/OrderPayTypeTransform.php
new file mode 100644
index 0000000..fc5db64
--- /dev/null
+++ b/app/Admin/Transforms/OrderPayTypeTransform.php
@@ -0,0 +1,26 @@
+"
+ : "";
+ }
+}
diff --git a/app/Admin/bootstrap.php b/app/Admin/bootstrap.php
new file mode 100644
index 0000000..aec7fe9
--- /dev/null
+++ b/app/Admin/bootstrap.php
@@ -0,0 +1,8 @@
+ config('admin.route.prefix'),
+ 'namespace' => config('admin.route.namespace'),
+ 'middleware' => config('admin.route.middleware'),
+], function (Router $router) {
+
+ $router->fallback('HomeController@noFound');
+ $router->get('/', 'HomeController@index');
+
+ // 覆盖默认的用户管理
+ $router->get('auth/users', 'AdminController@index');
+ // 覆盖默认的操作日志
+ $router->get('auth/logs', 'AdminController@indexLogs');
+
+ // 系统的配置
+ $router->resource('settings', 'SettingController')->only('index', 'store');
+
+ // 商品上架下架
+ $router->get('products/{id}/push', 'ProductController@pushProduct');
+
+ // 分类
+ // 商品
+ // 秒杀的商品管理
+ $router->resource('categories', 'CategoryController');
+ $router->resource('products', 'ProductController');
+ $router->resource('seckills', 'SeckillController')->only('index', 'create', 'store', 'destroy');
+
+ // 商品发货
+ // 管理员帮忙确认收货
+ $router->post('orders/{order}/ship', 'OrderController@ship');
+ $router->patch('orders/{order}/shipped', 'OrderController@confirmShip');
+
+ // 退款
+ // 订单
+ // 评论
+ $router->get('orders/{order}/refund', 'OrderController@refund');
+ $router->resource('orders', 'OrderController');
+ $router->resource('comments', 'CommentController');
+
+ // 会员管理
+ $router->resource('users', 'UserController');
+
+ // 积分日志
+ $router->get('score_logs', 'ScoreLogController@index');
+ // 用户购物车数据
+ $router->get('cars', 'CarController@index');
+ // 用户收藏数据
+ $router->get('user_like_products', 'ProductLikeController@index');
+
+
+ // 积分规则, 积分等级
+ $router->resource('score_rules', 'ScoreRuleController');
+ $router->resource('levels', 'LevelController');
+
+ // 优惠券管理
+ $router->resource('coupon_templates', 'CouponTemplateController');
+ // 优惠券
+ $router->resource('coupon_logs', 'CouponLogController')->only('index');
+ // 优惠券兑换码
+ $router->resource('coupon_codes', 'CouponCodeController')->only('index', 'create', 'store', 'destroy');
+
+ // 发布文章通知
+ $router->resource('article_notifications', 'ArticleNotificationController')->only('index', 'create', 'store', 'show', 'destroy');
+
+ // 富文本图片上传
+ $router->post('upload/editor', 'UploadController@uploadByEditor');
+ // 通过分类异步加载商品下拉列表
+ $router->get('api/products', 'CategoryController@getProducts');
+});
diff --git a/app/Console/Commands/AddShopToEsSearchCommand.php b/app/Console/Commands/AddShopToEsSearchCommand.php
new file mode 100644
index 0000000..739bdc5
--- /dev/null
+++ b/app/Console/Commands/AddShopToEsSearchCommand.php
@@ -0,0 +1,91 @@
+ping([
+ 'client' => [
+ 'timeout' => 5,
+ 'connect_timeout' => 5
+ ]
+ ]);
+ } catch (\Exception $exception) {
+
+ $this->info($exception->getMessage());
+ $this->info('无法连接到 elasticsearch 服务器,请配置 config/elasticsearch.php 文件');
+ $this->info('默认使用 MySQL 的模糊搜索');
+ $this->info('配置完毕后可运行: php artisan add:shop-to-search 添加索引');
+ return;
+ }
+
+
+ // 新建商品索引
+ if (Product::indexExists()) {
+
+ Product::deleteIndex();
+ $this->info('删除索引');
+ }
+ Product::createIndex();
+ $this->info('新建索引成功');
+
+
+ // 开始导入数据
+ $query = Product::query();
+
+ $count = $query->count();
+ $handle = 0;
+
+ $query->with('category')->chunk(1000, function (Collection $models) use ($count, &$handle) {
+
+ $models->map(function (Product $product) use ($count, &$handle) {
+
+ $product->addToIndex($product->getSearchData());
+
+ ++ $handle;
+ echo "\r {$handle}/$count";
+ });
+ });
+
+ echo PHP_EOL;
+ $this->info('索引生成完毕');
+ }
+}
diff --git a/app/Console/Commands/BaseCommand.php b/app/Console/Commands/BaseCommand.php
new file mode 100644
index 0000000..900f338
--- /dev/null
+++ b/app/Console/Commands/BaseCommand.php
@@ -0,0 +1,40 @@
+info('----------');
+ $this->info($command);
+
+ $output = shell_exec($command);
+
+ $this->info($output);
+ }
+
+}
diff --git a/app/Console/Commands/CacheOptimize.php b/app/Console/Commands/CacheOptimize.php
new file mode 100644
index 0000000..d447a20
--- /dev/null
+++ b/app/Console/Commands/CacheOptimize.php
@@ -0,0 +1,44 @@
+call('config:cache');
+ // 优化路由
+ $this->call('route:cache');
+ }
+}
diff --git a/app/Console/Commands/ClearCache.php b/app/Console/Commands/ClearCache.php
new file mode 100644
index 0000000..0820a0d
--- /dev/null
+++ b/app/Console/Commands/ClearCache.php
@@ -0,0 +1,42 @@
+call('config:clear');
+ $this->call('route:clear');
+ $this->call('view:clear');
+ }
+}
diff --git a/app/Console/Commands/CopyFile.php b/app/Console/Commands/CopyFile.php
new file mode 100644
index 0000000..38a15db
--- /dev/null
+++ b/app/Console/Commands/CopyFile.php
@@ -0,0 +1,66 @@
+filesystem = $filesystem;
+
+ parent::__construct();
+ }
+
+ /**
+ * 把静态资源发布到 public/storage 目录
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ // 图片的静态目录
+ $from = storage_path('app/resources/products');
+ $to = storage_path('app/public/products');
+ $this->filesystem->copyDirectory($from, $to);
+
+ // 默认头像
+ $from = storage_path('app/resources/avatars');
+ $to = storage_path('app/public/avatars');
+ $this->filesystem->copyDirectory($from, $to);
+
+ // 默认头像
+ $from = storage_path('app/resources/images');
+ $to = storage_path('app/public/images');
+ $this->filesystem->copyDirectory($from, $to);
+
+ $this->info('copy file success');
+ }
+}
diff --git a/app/Console/Commands/CountSite.php b/app/Console/Commands/CountSite.php
new file mode 100644
index 0000000..ad732de
--- /dev/null
+++ b/app/Console/Commands/CountSite.php
@@ -0,0 +1,59 @@
+subDay(1)->toDateString();
+
+ /**
+ * 防止一天运行多次,所以采用增加
+ *
+ * @var $site SiteCount
+ */
+ $site = SiteCount::query()->firstOrNew(compact('date'));
+ $site = $service->syncByCache($site, true);
+ $site->save();
+
+ createSystemLog('系统统计站点数据', $site->toArray());
+ }
+}
diff --git a/app/Console/Commands/DelExpireScoreData.php b/app/Console/Commands/DelExpireScoreData.php
new file mode 100644
index 0000000..65e1c4c
--- /dev/null
+++ b/app/Console/Commands/DelExpireScoreData.php
@@ -0,0 +1,53 @@
+subDay()->toDateString();
+
+ $serve = new ScoreLogServe();
+
+ Cache::delete($serve->loginKey($yesterday));
+
+ createSystemLog("系统删除{$yesterday}过期积分统计数据", ['date' => $yesterday]);
+ }
+}
diff --git a/app/Console/Commands/DelExpireSecKill.php b/app/Console/Commands/DelExpireSecKill.php
new file mode 100644
index 0000000..a7fbf4d
--- /dev/null
+++ b/app/Console/Commands/DelExpireSecKill.php
@@ -0,0 +1,92 @@
+where('is_rollback', 0)
+ ->where('end_at', '<', Carbon::now()->toDateTimeString())
+ ->get()
+ ->map(function (Seckill $seckill) use ($rollbacks) {
+
+ // 1. 回滚数量到商品
+ // 2. 设置为过期
+ $product = $seckill->product()->first();
+
+
+ // 获取 redis 数量
+ $jsonSeckill = Redis::get($seckill->getRedisModelKey());
+ $redisSeckill = json_decode($jsonSeckill, true);
+ // 获取剩余秒杀量
+ $surplus = Redis::llen($seckill->getRedisQueueKey());
+
+ // 恢复剩余的库存量
+ // 恢复库存数量
+ if ($redisSeckill['sale_count'] != 0) {
+ $product->increment('sale_count', $redisSeckill['sale_count']);
+ }
+
+ if ($surplus != 0) {
+ $product->increment('count', $surplus);
+ }
+
+
+ // 同步 redis 数据到数据库中
+ $seckill->sale_count += $redisSeckill['sale_count'];
+ $seckill->rollback_count += $surplus;
+ $seckill->is_rollback = 1;
+ $seckill->save();
+
+ $rollbacks->push($seckill);
+
+ // 删除掉秒杀数据
+ $ids = Redis::connection()->keys("seckills:{$seckill->id}:*");
+ Redis::del($ids);
+ });
+
+ if ($rollbacks->isNotEmpty()) {
+
+ createSystemLog('系统回滚秒杀数据', $rollbacks->toArray());
+ }
+ }
+}
diff --git a/app/Console/Commands/DeleteFile.php b/app/Console/Commands/DeleteFile.php
new file mode 100644
index 0000000..9ac5590
--- /dev/null
+++ b/app/Console/Commands/DeleteFile.php
@@ -0,0 +1,46 @@
+info('delete file success');
+ }
+}
diff --git a/app/Console/Commands/ExportDatabaseCommand.php b/app/Console/Commands/ExportDatabaseCommand.php
new file mode 100644
index 0000000..b6aae85
--- /dev/null
+++ b/app/Console/Commands/ExportDatabaseCommand.php
@@ -0,0 +1,56 @@
+oldest()
+ ->get()
+ ->transform(function (User $user) {
+
+ // 先排除自身主键
+ $user->offsetUnset($user->getKeyName());
+
+ return $user;
+ });
+
+ file_put_contents(\UsersTableSeeder::DATA_PATH, $users->toJson(JSON_UNESCAPED_UNICODE));
+ }
+}
diff --git a/app/Console/Commands/InstallShop.php b/app/Console/Commands/InstallShop.php
new file mode 100644
index 0000000..0a98115
--- /dev/null
+++ b/app/Console/Commands/InstallShop.php
@@ -0,0 +1,71 @@
+call('key:generate');
+ // 删除上一次保留的数据表
+ $this->call('migrate:reset');
+ // 删除上一次保留的文件
+ $this->call('moon:delete');
+
+
+ Product::$addToSearch = false;
+
+ /****************************************
+ * 1. 迁移数据表
+ * 2. 数据库迁移
+ * 3. 复制静态资源
+ * 4. 创建软链接
+ */
+ $this->call('migrate');
+ $this->call('db:seed');
+ $this->call('moon:copy');
+ $this->call('storage:link');
+
+ // 更新首页数据,防止上一次遗留
+ $this->call('moon:update-home');
+
+ // 生成全文索引
+ $this->call('add:shop-to-search');
+
+ // 直接开启监听队列
+ // $this->info('queue starting please don`t close cmd windows!!!');
+ // $this->call('queue:work', ['--tries' => '3']);
+ }
+}
diff --git a/app/Console/Commands/SendSubscribeEmail.php b/app/Console/Commands/SendSubscribeEmail.php
new file mode 100644
index 0000000..136c1c6
--- /dev/null
+++ b/app/Console/Commands/SendSubscribeEmail.php
@@ -0,0 +1,55 @@
+where('is_subscribe', 1)->pluck('email');
+
+ $mails->map(function ($realMail) {
+
+ $email = encrypt($realMail);
+ $url = route('site.email', compact('email'));
+ // 不要一次 to 多个用户,会暴露其他人的邮箱
+ Mail::to($realMail)->send(new SubscribesNotice($url));
+ });
+
+ createSystemLog('系统发送订阅消息, 发送的用户:' . $mails->implode(', '), $mails->toArray());
+ }
+}
diff --git a/app/Console/Commands/SyncProducViewCommand.php b/app/Console/Commands/SyncProducViewCommand.php
new file mode 100644
index 0000000..5510965
--- /dev/null
+++ b/app/Console/Commands/SyncProducViewCommand.php
@@ -0,0 +1,58 @@
+toDateString();
+
+ $products = Product::query()->where('today_has_view', 1)->get();
+
+ $products->map(function (Product $product) use ($yesterday) {
+
+
+ $viewCount = Cache::pull($product->getViewCountKey($yesterday), 0);
+ $product->view_count += $viewCount;
+ $product->today_has_view = 0;
+ $product->save();
+ });
+
+ createSystemLog("系统同步{$yesterday}商品浏览量", ['date' => $yesterday]);
+ }
+}
diff --git a/app/Console/Commands/UninstallShop.php b/app/Console/Commands/UninstallShop.php
new file mode 100644
index 0000000..01c6854
--- /dev/null
+++ b/app/Console/Commands/UninstallShop.php
@@ -0,0 +1,45 @@
+call('moon:clear');
+ $this->call('migrate:reset');
+
+ // delete all upload static resources
+ $this->call('moon:delete');
+ }
+}
diff --git a/app/Console/Commands/UpdateCacheHomeData.php b/app/Console/Commands/UpdateCacheHomeData.php
new file mode 100644
index 0000000..b0dcdea
--- /dev/null
+++ b/app/Console/Commands/UpdateCacheHomeData.php
@@ -0,0 +1,55 @@
+command(SendSubscribeEmail::class)->saturdays()->at('8:00');
+ // 每天统计注册人数, 销售数量
+ $schedule->command(CountSite::class)->dailyAt('01:00');
+ // 每小时执行一次, 回滚秒杀过期的数据
+ $schedule->command(DelExpireSecKill::class)->hourly();
+
+ // 每天夜里十二点执行删除昨天过期的积分数据
+ $schedule->command(DelExpireScoreData::class)->dailyAt('00:00');
+ // 每天夜里十二点同步商品浏览量
+ $schedule->command(SyncProducViewCommand::class)->dailyAt('00:10');
+
+ // 每分钟更新一次首页数据
+ $schedule->command(UpdateCacheHomeData::class)->everyMinute();
+ }
+
+ /**
+ * Register the commands for the application.
+ *
+ * @return void
+ */
+ protected function commands()
+ {
+ $this->load(__DIR__.'/Commands');
+
+ require base_path('routes/console.php');
+ }
+}
diff --git a/app/Enums/HomeCacheEnum.php b/app/Enums/HomeCacheEnum.php
new file mode 100644
index 0000000..609c975
--- /dev/null
+++ b/app/Enums/HomeCacheEnum.php
@@ -0,0 +1,17 @@
+
+ */
+ protected $dontFlash = [
+ 'current_password',
+ 'password',
+ 'password_confirmation',
+ ];
+
+ /**
+ * Register the exception handling callbacks for the application.
+ */
+ public function register(): void
+ {
+ $this->reportable(function (Throwable $e) {
+ //
+ });
+
+ $this->renderable(function (\Exception $exception, Request $request) {
+ if ($request->is('api*')) {
+
+ if ($exception instanceof JWTException) {
+
+ $mapExceptions = [
+ TokenInvalidException::class => '无效的token',
+ TokenBlacklistedException::class => 'token 已被加入黑名单,请重新登录'
+ ];
+
+ $msg = $mapExceptions[get_class($exception)] ?? $exception->getMessage();
+ return responseJsonAsUnAuthorized($msg);
+ }
+ // 拦截表单验证错误抛出的异常
+ elseif ($exception instanceof ValidationException) {
+
+ return responseJsonAsBadRequest($exception->validator->errors()->first());
+ }
+
+
+ return responseJsonAsServerError($exception->getMessage());
+ }
+
+
+ return responseJsonAsServerError($exception->getMessage(), null);
+ });
+ }
+}
diff --git a/app/Exceptions/OrderException.php b/app/Exceptions/OrderException.php
new file mode 100644
index 0000000..6900b5b
--- /dev/null
+++ b/app/Exceptions/OrderException.php
@@ -0,0 +1,15 @@
+input('username');
+ $password = $request->input('password');
+
+ $user = User::query()
+ ->whereNotNull('name')
+ ->where('name', $username)
+ ->first();
+
+ if (is_null($user)) {
+ return responseJsonAsBadRequest('用户名或者密码错误');
+ }
+
+ if (! Hash::check($password, $user->getAuthPassword())) {
+ return responseJsonAsBadRequest('用户名或者密码错误');
+ }
+
+
+ return responseJson(200, '登录成功', $this->getToken($user));
+ }
+
+ /**
+ * 注销的接口
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function logout()
+ {
+ auth('api')->logout();
+
+ return responseJsonAsDeleted('注销成功');
+ }
+
+ /**
+ * 注册的接口
+ *
+ * @param RegisterUserRequest $request
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function register(RegisterUserRequest $request)
+ {
+ $username = $request->input('username');
+ $password = $request->input('password');
+
+ if (User::query()->where('name', $username)->exists()) {
+
+ return responseJsonAsBadRequest('用户名已经存在, 请换一个用户名');
+ }
+
+ $user = new User();
+ $user->name = $username;
+ $user->password = $password;
+ $user->sex = UserSexEnum::MAN;
+ $user->is_init_email = 1;
+ // api 注册的用户默认激活
+ $user->is_active = UserStatusEnum::ACTIVE;
+ $user->save();
+
+
+ return responseJson(201, '注册成功', $this->getToken($user));
+ }
+
+
+
+ /**
+ * 拼接 token
+ *
+ * @param User $user
+ * @return array
+ */
+ protected function getToken(User $user)
+ {
+ // 换取 token
+ $prefix = 'Bearer';
+ $token = auth('api')->login($user);
+ $me = new OwnResource($user);
+
+ return compact('prefix', 'token', 'me');
+ }
+}
diff --git a/app/Http/Controllers/Api/V1/CategoryController.php b/app/Http/Controllers/Api/V1/CategoryController.php
new file mode 100644
index 0000000..35a7ed9
--- /dev/null
+++ b/app/Http/Controllers/Api/V1/CategoryController.php
@@ -0,0 +1,73 @@
+getPageParameters();
+
+ $query = Category::query();
+
+ if ($title = $serve->input('name')) {
+
+ $query->where('title', 'like', "%{$title}%");
+ }
+
+
+ $count = $query->count();
+ $categories = $query->orderBy('order')->limit($limit)->offset($offset)->get();
+ $categories = CategoreResource::collection($categories);
+
+ return responseJson(200, 'success', $categories, compact('count'));
+ }
+
+
+ public function getProducts(PageServe $serve, $category)
+ {
+ list($limit, $offset) = $serve->getPageParameters();
+
+ // 排序的字段和排序的值
+ $orderField = $serve->input('order_field');
+ $orderValue = $serve->input('order_value');
+
+ /**
+ * @var $category Category
+ */
+ $category = Category::query()->findOrFail($category);
+
+ $query = $category->products();
+
+
+ if ($name = $serve->input('name')) {
+
+ $query->where('name', 'like', "%{$name}%");
+ }
+
+ // 获取排序的字段
+ $allFields = ['created_at', 'sale_count', 'view_count'];
+ $orderField = in_array($orderField, $allFields) ?
+ $orderField :
+ array_first($allFields);
+ $orderValue = $orderValue === 'asc' ? 'asc' : 'desc';
+
+
+ // 获取数据
+ $count = $query->count();
+ $products = $query->orderBy($orderField, $orderValue)
+ ->limit($limit)
+ ->offset($offset)
+ ->get();
+ $products = ProductResource::collection($products);
+
+ return responseJson(200, 'success', $products, compact('count'));
+ }
+}
diff --git a/app/Http/Controllers/Api/V1/HomeController.php b/app/Http/Controllers/Api/V1/HomeController.php
new file mode 100644
index 0000000..66e1b62
--- /dev/null
+++ b/app/Http/Controllers/Api/V1/HomeController.php
@@ -0,0 +1,14 @@
+user();
+
+ return responseJson(200, 'success', new OwnResource($me));
+ }
+
+ public function scoreLogs(PageServe $serve)
+ {
+ list($limit, $offset) = $serve->getPageParameters();
+ /**
+ * @var $me User
+ */
+ $me = auth()->user();
+
+ $query = $me->scoreLogs();
+
+ $count = $query->count();
+ $scoreLogs = $me->scoreLogs()
+ ->latest()
+ ->offset($offset)
+ ->limit($limit)
+ ->get();
+
+
+ return responseJson(200, 'success', ScoreLogResource::collection($scoreLogs), compact('count'));
+ }
+}
diff --git a/app/Http/Controllers/Api/V1/ProductController.php b/app/Http/Controllers/Api/V1/ProductController.php
new file mode 100644
index 0000000..c3b1d88
--- /dev/null
+++ b/app/Http/Controllers/Api/V1/ProductController.php
@@ -0,0 +1,25 @@
+where('uuid', $uuid)->firstOrFail();
+ $product->load('detail');
+
+ // 直接使用缓存
+ $today = Carbon::today()->toDateString();
+ Cache::increment($product->getViewCountKey($today));
+
+ return responseJson(200, 'success', new ProductResource($product));
+ }
+}
diff --git a/app/Http/Controllers/Auth/AuthLoginController.php b/app/Http/Controllers/Auth/AuthLoginController.php
new file mode 100644
index 0000000..0ac5f25
--- /dev/null
+++ b/app/Http/Controllers/Auth/AuthLoginController.php
@@ -0,0 +1,161 @@
+allow) || ! config()->has("socialite.{$driver}")) {
+
+ abort(403, '未知的第三方登录');
+ }
+
+ $socialite = new SocialiteManager(config('socialite'), request());
+
+ return $socialite->driver($driver)->redirect();
+ }
+
+
+ /**
+ * 第三方授权认证回调
+ *
+ * @param $driver
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\View\View
+ */
+ public function handleCallback($driver)
+ {
+
+ if (! in_array($driver, $this->allow) || ! config()->has("socialite.{$driver}")) {
+
+ abort(403, '未知的第三方登录');
+ }
+
+ try {
+
+ $socialite = new SocialiteManager(config('socialite'), request());
+ $socialiteUser = $socialite->driver($driver)->user();
+ } catch (AuthorizeFailedException $e) {
+
+ return view('hint.error', ['status' => $e->getMessage(), 'url' => route('login')]);
+ }
+
+ /**
+ * 处理第三方登录用户信息
+ *
+ * @var $user User
+ */
+ $user = $this->findOrCreateMatchUser($socialiteUser);
+
+ // 如果用户已经登录的,作为绑定账号。跳转到个人中心页面
+ if (auth()->check()) {
+
+ return redirect('/user/setting')->with('status', '绑定成功');
+ }
+
+ // 第三方如果没有登录,那么主动登录
+ auth()->login($user, true);
+ // 登录次数
+ $user->increment('login_count');
+
+ // 如果 session 中有跳转 url,则跳转
+ return redirect()->intended();
+ }
+
+ /**
+ * 找到数据库匹配的记录,并存储用户
+ *
+ * @param \Overtrue\Socialite\User $socialiteUser
+ * @return mixed
+ */
+ protected function findOrCreateMatchUser(\Overtrue\Socialite\User $socialiteUser)
+ {
+ // 新建用户
+ $driver = strtoupper($socialiteUser->getProviderName());
+ $idField = "{$driver}_id";
+ $nameField = "{$driver}_name";
+
+ /**
+ * 如果是已经登录的用户
+ * @var $user User
+ */
+ if ($user = auth()->user()) {
+ $user->setAttribute($idField, $socialiteUser->getId())
+ ->setAttribute($nameField, $socialiteUser->getName())
+ ->save();
+
+ return $user;
+ }
+
+ // 如果用户没有登录,就是使用第三方账号登录
+ // 如果数据库没有记录就创建,有就修改一下显示名
+ $user = User::query()->firstOrNew([$idField => $socialiteUser->getId()]);
+ $user->$nameField = $socialiteUser->getName();
+ // 用户的来源
+ $sources = UserSourceEnum::toArray();
+ $user->source = $sources[$driver] ?? array_first($sources);
+
+ // 如果用户不存在
+ if (! $user->exists) {
+
+ if ($socialiteUser->getAvatar()) {
+ $user->avatar = $socialiteUser->getAvatar();
+ }
+
+ // 用户的密码是初始的,可以不用输入旧密码修
+ //// 使用第三方登录的用户,默认激活
+ $user->is_active = 1;
+ $user->is_init_name = 1;
+ $user->is_init_email = 1;
+ $user->is_init_password = 1;
+ }
+
+ $user->save();
+
+ return $user;
+ }
+
+
+ /**
+ * 解绑第三方账号
+ *
+ * @param $driver
+ * @param Request $request
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ public function unBind($driver, Request $request)
+ {
+ if (! in_array($driver, $this->allow) || ! config()->has("socialite.{$driver}")) {
+
+ return back()->withErrors(['msg' => '未知的第三方登录']);
+ }
+
+
+ // 可以做更多的判断,如用 QQ 注册的不能解绑之类的
+ /**
+ * @var $user User
+ */
+ $idField = "{$driver}_id";
+ $nameField = "{$driver}_name";
+ $user = $request->user();
+ $user->setAttribute($idField, null)->setAttribute($nameField, null)->save();
+
+
+ return back()->with('status', '解绑成功');
+ }
+}
diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php
new file mode 100644
index 0000000..41fc004
--- /dev/null
+++ b/app/Http/Controllers/Auth/ForgotPasswordController.php
@@ -0,0 +1,39 @@
+middleware('guest');
+ }
+
+ protected function guard()
+ {
+ return auth()->guard();
+ }
+
+}
diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php
new file mode 100644
index 0000000..4b79f19
--- /dev/null
+++ b/app/Http/Controllers/Auth/LoginController.php
@@ -0,0 +1,167 @@
+middleware('guest')->except('logout');
+ }
+
+
+ /**
+ * 登录页面
+ *
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function showLoginForm()
+ {
+ $lastUrl = URL::previous();
+
+
+ // 记录上一次的 url,用于登录之后的回跳
+ if (!str_is($this->except, $lastUrl)) {
+
+ session()->put('url.intended', $lastUrl);
+ }
+
+ return view('auth.login');
+ }
+
+ /**
+ * @param Request $request
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response|void
+ * @throws \Illuminate\Validation\ValidationException
+ */
+ public function login(Request $request, UserService $userService)
+ {
+ $this->validateLogin($request);
+
+ // 如果超过限制登录次数
+ if ($this->hasTooManyLoginAttempts($request)) {
+ $this->fireLockoutEvent($request);
+
+ $this->sendLockoutResponse($request);
+ }
+
+ /**
+ * @var $user User
+ */
+ $credentials = $this->credentials($request);
+ $user = User::query()->where($credentials)->first();
+
+ if ($user instanceof User && \Hash::check($request->input('password'), $user->password)) {
+
+ // 如果用户没有激活
+ if ($user->is_active == UserStatusEnum::UN_ACTIVE) {
+
+ // 显示 再次发送激活链接
+ return redirect('login')->withInput()
+ ->withErrors([
+ $this->username() => $userService->getActiveLink($user)
+ ]);
+ }
+
+ // 登录用户
+ auth()->login($user, $request->has('remember'));
+ $user->increment('login_count');
+ return $this->sendLoginResponse($request);
+ }
+
+ // 如果登录尝试不成功,我们将增加数量
+ // 登录并将用户重定向到登录表单。当然,当这个
+ // 超过最大尝试次数的用户将被锁定。
+ $this->incrementLoginAttempts($request);
+
+ return $this->sendFailedLoginResponse($request);
+ }
+
+
+ protected function sendFailedLoginResponse(Request $request)
+ {
+ throw ValidationException::withMessages([
+ $this->username() => [trans('auth.failed')],
+ ]);
+ }
+
+ protected function sendLoginResponse(Request $request)
+ {
+ $request->session()->regenerate();
+
+ $this->clearLoginAttempts($request);
+
+ return redirect()->intended($this->redirectPath());
+ }
+ /**
+ * 登录使用用户名还是邮箱
+ * @param Request $request
+ * @return array
+ */
+ protected function credentials(Request $request)
+ {
+ $input = $request->input($this->username());
+ $field = filter_var($input, FILTER_VALIDATE_EMAIL) ? 'email' : 'name';
+
+ return [
+ $field => $input,
+ ];
+ }
+
+ protected function validateLogin(Request $request)
+ {
+ $this->validate($request, [
+ $this->username() => 'required|string',
+ 'password' => 'required|string',
+ ], [
+ $this->username() . '.required' => '账号不能为空',
+ $this->username() . '.string' => '账号必须是正确的字符串',
+ 'password.required' => '密码不能为空'
+ ]);
+ }
+
+ /**
+ * Log the user out of the application.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @return \Illuminate\Http\Response
+ */
+ public function logout(Request $request)
+ {
+ auth()->logout();
+
+ $request->session()->invalidate();
+
+ return redirect('/');
+ }
+
+ protected function username()
+ {
+ return 'account';
+ }
+}
diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php
new file mode 100644
index 0000000..523db3d
--- /dev/null
+++ b/app/Http/Controllers/Auth/RegisterController.php
@@ -0,0 +1,145 @@
+middleware('guest');
+ }
+
+
+ /**
+ * 核心注册方法
+ *
+ * @param Request $request
+ * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ * @throws \Illuminate\Validation\ValidationException
+ */
+ public function register(Request $request)
+ {
+ $this->validator($request->all())->validate();
+
+ if (strtolower($request->input('captcha')) != strtolower(session('captcha'))) {
+
+ return redirect('/register')->withErrors(['captcha' => '验证码不正确']);
+ }
+
+
+ event(new Registered($user = $this->create($request->all())));
+
+ $this->registered($request, $user);
+ return redirect('/login')->with('status', '注册成功');
+ }
+
+ /**
+ * registered event (send email)
+ * @param Request $request
+ * @param $user
+ */
+ protected function registered(Request $request, $user)
+ {
+ Mail::to($user->email)
+ ->queue(new UserRegister($user));
+ }
+
+
+ protected function validator(array $data)
+ {
+ return Validator::make($data, [
+ 'name' => 'required|string|max:50|unique:users',
+ 'email' => 'required|string|email|max:50|unique:users',
+ 'password' => 'required|string|min:5|confirmed',
+ 'sex' => ['required', Rule::in([UserSexEnum::MAN, UserSexEnum::WOMAN])],
+ 'captcha' => 'required',
+ ], [
+ 'name.required' => '用户名不能为空',
+ 'name.max' => '用户名不能超过50个字符',
+ 'name.unique' => '用户名已经被占用',
+ 'email.unique' => '邮箱已经被占用',
+ 'email.required' => '邮箱不能为空',
+ 'email.email' => '邮箱格式不正确',
+ 'password.min' => '密码最少六位数',
+ 'password.required' => '密码不能为空',
+ 'password.confirmed' => '两次密码不一致',
+ 'captcha.required' => '验证码不能为空',
+ 'sex.in' => '性别错误',
+ ]);
+ }
+
+ protected function create(array $data)
+ {
+ // email_active,
+ return User::query()->create([
+ 'name' => $data['name'],
+ 'email' => $data['email'],
+ 'sex' => $data['sex'],
+ 'password' => bcrypt($data['password']),
+ 'active_token' => str_random(60),
+ 'is_active' => 1,
+ ]);
+
+ }
+
+ protected function redirectTo()
+ {
+ return 'register';
+ }
+
+
+ /**
+ * @return string
+ */
+ public function captcha()
+ {
+ $builder = (new CaptchaBuilder(4))->build(150, 46);
+
+ session()->put('captcha', $builder->getPhrase());
+
+ return $builder->get();
+ }
+
+ protected function guard()
+ {
+ return auth()->guard();
+ }
+}
diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php
new file mode 100644
index 0000000..4ac611d
--- /dev/null
+++ b/app/Http/Controllers/Auth/ResetPasswordController.php
@@ -0,0 +1,79 @@
+middleware('guest');
+ }
+
+ public function validationErrorMessages()
+ {
+ return [
+ 'token.required' => '重置密码的token不是对应这个邮箱',
+ 'email.required' => '邮件地址不正确',
+ 'email.email' => '邮件地址不正确',
+ 'password.required' => '密码不能为空',
+ 'password.confirmed' => '两次密码不一致',
+ 'password.min' => '密码不能少于六位数',
+ ];
+ }
+
+ /**
+ * rewrite Illuminate\Foundation\Auth\ResetsPasswords::resetPassword not login
+ * @param $user
+ * @param $password
+ */
+ public function resetPassword($user, $password)
+ {
+ $user->password = Hash::make($password);
+ $user->setRememberToken(Str::random(60));
+ $user->save();
+
+ // event(new PasswordReset($user));
+ // $this->guard()->login($user);
+ }
+
+ public function redirectTo()
+ {
+ return 'password/reset';
+ }
+
+ protected function guard()
+ {
+ return auth()->guard();
+ }
+}
diff --git a/app/Http/Controllers/Auth/UserController.php b/app/Http/Controllers/Auth/UserController.php
new file mode 100644
index 0000000..f5d2d2c
--- /dev/null
+++ b/app/Http/Controllers/Auth/UserController.php
@@ -0,0 +1,39 @@
+where('active_token', $token)->first()) {
+ $user->is_active = 1;
+ // 重新生成激活token
+ $user->active_token = str_random(60);
+ $user->save();
+
+ return view('hint.success', ['status' => "{$user->name} 账户激活成功!", 'url' => url('login')]);
+ } else {
+ return view('hint.error', ['status' => '无效的token']);
+ }
+ }
+
+ public function sendActiveMail($id)
+ {
+ if ($user = User::query()->find($id)) {
+ // again send active link, join queue
+ Mail::to($user->email)
+ ->queue(new UserRegister($user));
+
+ return view('hint.success', ['status' => '发送邮件成功', 'url' => route('login')]);
+
+ }
+
+ return view('hint.error', ['status' => '用户名或者密码错误']);
+ }
+}
diff --git a/app/Http/Controllers/CarController.php b/app/Http/Controllers/CarController.php
new file mode 100644
index 0000000..d1a0bd5
--- /dev/null
+++ b/app/Http/Controllers/CarController.php
@@ -0,0 +1,105 @@
+middleware('user.auth')->only('store', 'destroy');
+ }
+
+ /**
+ * 购物车列表
+ *
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function index()
+ {
+ $cars = collect();
+
+ /**
+ * @var $user User
+ */
+ if ($user = \auth()->user()) {
+ // 直接获取当前登录用户的购物车
+ $cars = $user->cars()->with('product')->get();
+ }
+
+ return view('cars.index', compact('cars'));
+ }
+
+ /**
+ * 添加购物车
+ * @param Request $request
+ * @return array
+ */
+ public function store(Request $request)
+ {
+ /**
+ * @var $car Car
+ * @var $product Product
+ * @var $user User
+ */
+ $product = Product::query()->where('uuid', $request->input('product_id'))->firstOrFail();
+
+ $user = auth()->user();
+ $car = $user->cars()->firstOrNew([
+ 'user_id' => \auth()->id(),
+ 'product_id' => $product->id
+ ]);
+
+
+ // 如果是同步,则只是赋值,如果是添加购物车则是添加
+ $change = 0;
+ $number = $request->input('number', 1);
+
+ if ($request->input('action') == 'sync') {
+
+ $change = $number - $car->number;
+ $car->number = $number;
+ } else {
+
+ $car->number += $number;
+ }
+
+ if ($car->number > $product->count) {
+
+ return responseJson(403, '库存不足');
+ }
+
+
+ $car->save();
+
+ return responseJson(200, '加入购物车成功', compact('change'));
+ }
+
+
+ /**
+ * @param $id
+ * @return array
+ */
+ public function destroy($id)
+ {
+ try {
+ /**
+ * @var $user User
+ */
+ $user = auth()->user();
+ $car = $user->cars()->whereKey($id)->firstOrFail();
+ $car->delete();
+
+ } catch (\Exception $e) {
+
+ dd($e);
+ return responseJson(500, '服务器异常,请稍后再试');
+ }
+
+ return responseJson(200, '删除成功');
+ }
+}
diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php
new file mode 100644
index 0000000..a20c69c
--- /dev/null
+++ b/app/Http/Controllers/CategoryController.php
@@ -0,0 +1,40 @@
+latest()->paginate(30);
+
+
+ return view('categories.index', compact('categories'));
+ }
+
+ /**
+ * 分类详情
+ *
+ * @param Request $request
+ * @param Category $category
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function show(Request $request, Category $category)
+ {
+ $orderBy = $request->input('orderBy', 'created_at');
+ $categoryProducts = $category->products()->withCount('users')->orderBy($orderBy, 'desc')->paginate(10);
+
+
+ return view('categories.show', compact('category', 'categoryProducts'));
+ }
+}
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
new file mode 100644
index 0000000..03e02a2
--- /dev/null
+++ b/app/Http/Controllers/Controller.php
@@ -0,0 +1,13 @@
+input('template_id');
+
+ /**
+ * @var $user User
+ */
+ $user = auth()->user();
+ if (is_null($user)) {
+
+ return responseJsonAsUnAuthorized('请先登录再领取优惠券');
+ }
+
+ $template = CouponTemplate::query()->find($templateId);
+ if (is_null($template)) {
+
+ return responseJsonAsBadRequest('无效的优惠券');
+ }
+
+ // 判断优惠券是否过期
+ $today = Carbon::today();
+ $endDate = Carbon::make($template->end_date);
+ if ($today->gt($endDate)) {
+
+ return responseJsonAsBadRequest('优惠券已过期');
+ }
+
+ if ($user->score_now < $template->score) {
+
+ return responseJsonAsBadRequest("积分不足{$template->score},请先去获取积分");
+ }
+
+ // 这里我会只让每个用户只能领取一张优惠券
+ if ($user->coupons()->where('template_id', $templateId)->exists()) {
+
+ return responseJsonAsBadRequest('你已经领取过优惠券了');
+ }
+
+ DB::beginTransaction();
+
+ try {
+
+ // 用户减少积分
+ if ($template->score > 0) {
+
+ $user->score_now -= $template->score;
+ $user->save();
+ // 生成积分日志
+ $log = new ScoreLog();
+ $log->user_id = $user->getKey();
+ $log->description = "领取优惠券";
+ $log->score = -1 * $template->score;
+ $log->save();
+ }
+
+ // 开始领取优惠券
+ $coupon = new UserHasCoupon();
+ $coupon->template_id = $template->getKey();
+ $coupon->user_id = $user->getKey();
+
+ $coupon->title = $template->title;
+ $coupon->amount = $template->amount;
+ $coupon->full_amount = $template->full_amount;
+ $coupon->start_date = $template->start_date;
+ $coupon->end_date = $template->end_date;
+ $coupon->save();
+
+ } catch (\Exception $e) {
+
+ DB::rollBack();
+ return responseJsonAsServerError('服务器异常,请稍后再试');
+ }
+
+ DB::commit();
+
+ return responseJson(200, '领取成功');
+ }
+}
diff --git a/app/Http/Controllers/CouponTemplateController.php b/app/Http/Controllers/CouponTemplateController.php
new file mode 100644
index 0000000..089b599
--- /dev/null
+++ b/app/Http/Controllers/CouponTemplateController.php
@@ -0,0 +1,28 @@
+toDateString();
+
+ // 只查询未过期的
+ // 标记已经领取过的
+ $templates = CouponTemplate::query()
+ ->withCount(['coupons' => function ($b) {
+
+ $b->where('user_id', auth()->id());
+ }])
+ ->where('end_date', '>=', $today)
+ ->get();
+
+
+ return view('coupons.templates', compact('templates'));
+ }
+}
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
new file mode 100644
index 0000000..bf49e03
--- /dev/null
+++ b/app/Http/Controllers/HomeController.php
@@ -0,0 +1,67 @@
+user()) {
+
+ $loginUser->load('subscribe');
+ }
+
+ // 查询优惠券
+ $couponTemplates = HomeCacheDataUtil::couponTemplates();
+
+ $setting = new SettingKeyEnum(SettingKeyEnum::IS_OPEN_SECKILL);
+ $isOpenSeckill = setting($setting) == 1;
+
+ return view(
+ 'homes.index',
+ compact('categories', 'hotProducts', 'latestProducts', 'users', 'secKills', 'loginUser', 'isOpenSeckill', 'couponTemplates')
+ );
+ }
+
+
+ public function unSubscribe($email)
+ {
+
+ try {
+ $email = decrypt($email);
+ } catch (\Exception $e) {
+
+ return view('hint.error', ['status' => '未知的账号']);
+ }
+
+ Subscribe::query()->where('email', $email)->update(['is_subscribe' => 0]);
+ return view('hint.success', ['status' => '已取消订阅']);
+ }
+}
diff --git a/app/Http/Controllers/PaymentNotificationController.php b/app/Http/Controllers/PaymentNotificationController.php
new file mode 100644
index 0000000..137e498
--- /dev/null
+++ b/app/Http/Controllers/PaymentNotificationController.php
@@ -0,0 +1,95 @@
+config = config('pay.ali');
+ }
+
+
+ /**
+ * 后台通知的接口
+ *
+ * @param Request $request
+ * @return \Symfony\Component\HttpFoundation\Response
+ */
+ public function payNotify(Request $request)
+ {
+ $alipay = Pay::alipay($this->config);
+
+ // TODO , 加一个轮询接口配合后台通知修改订单状态
+ // 后台异步通知接口有可能会因为网络问题接收不到
+ // 使用轮询插接订单状态,如果支付了停止轮询
+ try{
+ $data = $alipay->verify(); // 是的,验签就这么简单!
+
+ // 验证 app_id
+ // 可:判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额);
+ if ($data->get('app_id') == $this->config['app_id']) {
+
+ // 支付成功
+ if ($data->get('trade_status') == 'TRADE_SUCCESS') {
+
+ // 更新订单
+ $order = Order::query()->where('no', $data->get('out_trade_no'))->firstOrFail();
+ $order->paid_at = $data->get('notify_time');
+ $order->pay_no = $data->get('trade_no');
+ $order->pay_amount = $data->get('receipt_amount');
+ $order->status = OrderStatusEnum::PAID;
+ $order->pay_type = OrderPayTypeEnum::ALI;
+ $order->save();
+ }
+ }
+
+ Log::debug('Alipay notify', $data->all());
+ } catch (\Exception $e) {
+
+ Log::debug('Alipay notify', $e->getMessage());
+ }
+
+ return $alipay->success();
+ }
+
+
+ /**
+ * 前台跳转的接口
+ *
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function payReturn()
+ {
+ $latestProducts = Product::query()->latest()->take(9)->get();
+
+ $order = null;
+
+ try {
+
+ $data = Pay::alipay($this->config)->verify();
+
+ $order = Order::query()->where('no', $data->get('out_trade_no'))->firstOrFail();
+
+ } catch (\Exception $e) {
+
+ }
+
+ return view('user.payments.result', compact('order', 'latestProducts'));
+ }
+
+
+}
diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php
new file mode 100644
index 0000000..1a5f867
--- /dev/null
+++ b/app/Http/Controllers/ProductController.php
@@ -0,0 +1,147 @@
+inRandomOrder()->take(9)->get(['uuid', 'name'])->split(3);
+ $pinyins = ProductPinYin::query()->orderBy('pinyin')->pluck('pinyin');
+
+ return view('products.index', compact('products', 'pinyins'));
+ }
+
+
+ /**
+ * ajax 通过商品首字母查询商品
+ * @param $pinyin
+ * @return mixed
+ */
+ public function getProductsByPinyin($pinyin)
+ {
+ $products = Product::query()->where('first_pinyin', $pinyin)->get(['id', 'name'])->split(3);
+
+ return $products;
+ }
+
+
+ /**
+ * 商品搜索
+ *
+ * @param Request $request
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function search(Request $request)
+ {
+ $keyword = $request->input('keyword', '');
+ $page = abs((int)$request->get('page', 1));
+ $limit = (int)$request->get('limit', 20);
+ $offset = (int) ($page - 1) * $limit;
+
+ // 全文索引
+ try {
+
+ $parameters = [
+ 'multi_match' => [
+ 'query' => $keyword,
+ 'fields' => ['title', 'body'],
+ ]
+ ];
+
+ $count = Product::searchCount($parameters);
+ $searchCount = $count['count'] ?? 0;
+ $searchResult = Product::search($parameters, $limit, $offset);
+ $filterIds = Collection::make($searchResult['hits']['hits'] ?? [])->pluck('_source.id');
+ $models = Product::query()->findMany($filterIds);
+
+ $products = new LengthAwarePaginator($models, $searchCount, $limit, $page);
+
+ } catch (\Exception $e) {
+
+ $products = Product::query()->withCount('users')->where('name', 'like', "%{$keyword}%")->paginate($limit);
+ }
+
+ return view('products.search', compact('products'));
+ }
+
+ /**
+ * 单个商品显示
+ *
+ * @param $uuid
+ * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
+ */
+ public function show($uuid)
+ {
+ /**
+ * @var $user User|null
+ */
+ $product = Product::query()->where('uuid', $uuid)->firstOrFail();
+
+
+ if (! $product->today_has_view) {
+ $product->today_has_view = true;
+ $product->save();
+ }
+ // 直接使用缓存
+ $today = Carbon::today()->toDateString();
+ Cache::increment($product->getViewCountKey($today));
+
+
+ // 商品浏览次数 + 1
+ $user = auth()->user();
+
+ // 同类商品推荐
+ $recommendProducts = Product::query()
+ ->where('category_id', $product->category_id)
+ ->take(5)
+ ->get();
+
+ // 加载出详情,收藏的人数, 评论
+ $product->load([
+ 'detail',
+ 'users',
+ 'comments' => function ($query) {
+ $query->latest();
+ },
+ 'comments.user'
+ ]);
+ $product->userIsLike = $product->users()->where('id', auth()->id())->exists();
+
+ // 如果登录返回所有地址列表,如果没有,则返回一个空集合
+ if ($user) {
+
+ // 浏览商品增加积分
+ (new ScoreLogServe)->visitedProductAddScore($user, $product);
+ }
+
+ return view('products.show', compact('product', 'recommendProducts'));
+ }
+
+ /**
+ * @return \Illuminate\Contracts\Auth\StatefulGuard
+ */
+ protected function guard()
+ {
+ return auth()->guard();
+ }
+}
diff --git a/app/Http/Controllers/User/AddressController.php b/app/Http/Controllers/User/AddressController.php
new file mode 100644
index 0000000..ba85772
--- /dev/null
+++ b/app/Http/Controllers/User/AddressController.php
@@ -0,0 +1,121 @@
+user()->addresses;
+
+ // Provincial and municipal regions
+ $provinces = DB::table('provinces')->get();
+ $cities = DB::table('cities')->where('province_id', $provinces->first()->id)->get();
+
+ return view('user.addresses.index', compact('addresses', 'provinces', 'cities'));
+ }
+
+
+ public function store(AddressRequest $request)
+ {
+ $addressesData = $this->getFormatRequest($request);
+
+ //
+ /**
+ * @var $user User
+ */
+ $user = auth()->user();
+ $user->addresses()->create($addressesData);
+
+ return back()->with('status', '创建成功');
+ }
+
+
+ public function show(Address $address)
+ {
+ return $address;
+ }
+
+
+ public function edit(Address $address)
+ {
+ if (auth()->id() != $address->user_id) {
+
+ abort(403, '非法操作');
+ }
+
+ $addresses = auth()->user()->addresses;
+ // Provincial and municipal regions
+ $provinces = DB::table('provinces')->get();
+ $cities = DB::table('cities')->where('province_id', $address->province_id)->get();
+
+ return view('user.addresses.edit', compact('addresses', 'address', 'provinces', 'cities'));
+ }
+
+
+ public function update(AddressRequest $request, Address $address)
+ {
+ if (auth()->id() != $address->user_id) {
+
+ abort(403, '非法操作');
+ }
+
+ $addressesData = $this->getFormatRequest($request);
+
+ $address->update($addressesData);
+
+ return back()->with('status', '修改成功');
+ }
+
+ public function destroy(Address $address)
+ {
+ if (auth()->id() != $address->user_id) {
+
+ return responseJson(400, '非法操作');
+ }
+
+ $address->delete();
+
+ return responseJson(200, '删除成功');
+ }
+
+
+ public function setDefaultAddress(Address $address)
+ {
+ if (auth()->id() != $address->user_id) {
+
+ return responseJson(400, '非法操作');
+ }
+
+ Address::query()->where('user_id', $address->user_id)->update(['is_default' => 0]);
+ $address->is_default = 1;
+
+ if ($address->save()) {
+
+ return responseJson(0, '设置成功');
+ }
+
+ return responseJson(400, '请稍后再试!');
+ }
+
+
+ protected function getFormatRequest(Request $request)
+ {
+ return $request->only(['name', 'phone', 'province_id', 'city_id','detail_address']);
+ }
+
+
+ public function getCities(Request $request)
+ {
+ return DB::table('cities')->where('province_id', $request->input('province_id'))->get();
+ }
+}
diff --git a/app/Http/Controllers/User/LikesController.php b/app/Http/Controllers/User/LikesController.php
new file mode 100644
index 0000000..c386fae
--- /dev/null
+++ b/app/Http/Controllers/User/LikesController.php
@@ -0,0 +1,65 @@
+ 1,
+ 'msg' => '服务器异常,请稍后再试',
+ ];
+
+
+ public function index()
+ {
+ /**
+ * @var $user User
+ */
+ $user = auth()->user();
+
+ $likesProducts = $user->products()
+ ->where('user_id', auth()->id())
+ ->withCount('users')
+ ->latest()
+ ->paginate(10);
+
+ return view('user.products.likes', compact('likesProducts'));
+ }
+
+
+ public function toggle($uuid)
+ {
+ /**
+ * @var $product Product
+ */
+ $product = Product::query()
+ ->where('uuid', $uuid)
+ ->firstOrFail();
+
+
+ $user = auth()->id();
+
+ if ($product->users()->where('user_id', $user)->exists()) {
+
+ $product->users()->detach($user);
+
+ return response()->json([
+ 'code' => 200,
+ 'msg' => '欢迎下次收藏'
+ ]);
+ }
+
+ $product->users()->attach($user);
+
+ return response()->json([
+ 'code' => 201,
+ 'msg' => '收藏成功'
+ ]);
+}
+}
diff --git a/app/Http/Controllers/User/NotificationController.php b/app/Http/Controllers/User/NotificationController.php
new file mode 100644
index 0000000..6fbba21
--- /dev/null
+++ b/app/Http/Controllers/User/NotificationController.php
@@ -0,0 +1,149 @@
+user();
+
+
+ switch ($request->input('tab', 1)) {
+
+ case 2:
+ $query = $user->notifications();
+ break;
+ case 3:
+ $query = $user->readNotifications();
+ break;
+ case 1:
+ default:
+ $query = $user->unreadNotifications();
+ break;
+ }
+
+ $notifications = $query->paginate();
+
+ foreach ($notifications as $notification) {
+
+ $notification->title = NotificationServe::getTitle($notification);
+ }
+
+ $unreadCount = $user->unreadNotifications()->count();
+ $readCount = $user->readNotifications()->count();
+
+ return view('user.notifications.index', compact('notifications', 'unreadCount', 'readCount'));
+ }
+
+ public function read($id)
+ {
+ /**
+ * @var $user User
+ */
+ $user = auth()->user();
+
+ /**
+ * @var $notification DatabaseNotification
+ */
+ $notification = $user->notifications()->find($id);
+ if (is_null($notification)) {
+
+ return responseJsonAsBadRequest('无效的通知');
+ }
+
+ $notification->markAsRead();
+
+ return responseJson();
+ }
+
+ public function readAll()
+ {
+ /**
+ * @var $user User
+ */
+ $user = auth()->user();
+
+
+ $count = $user->unreadNotifications()->update(['read_at' => Carbon::now()]);
+
+
+ return responseJson(200, "本次已读{$count}条消息");
+ }
+
+ public function show($id)
+ {
+
+ /**
+ * @var $user User
+ */
+ $user = auth()->user();
+
+ /**
+ * @var $notification DatabaseNotification
+ */
+ $notification = $user->notifications()->find($id);
+ if (is_null($notification)) {
+
+ return abort(403, '无效的通知');
+ }
+
+ // 查看是否有上一条下一条
+ $last = $user->notifications()->where('created_at', '<', $notification->created_at)->first();
+ $next = $user->notifications()->where('created_at', '>', $notification->created_at)->first();
+ // 标记为已读
+ $notification->markAsRead();
+
+ $view = NotificationServe::getView($notification);
+ if (! view()->exists($view)) {
+
+ abort(404, '未知的的消息');
+ }
+
+ $notification->title = NotificationServe::getTitle($notification);
+ $data = $notification->data;
+
+ return view('user.notifications.show', compact('last', 'next', 'notification', 'view', 'data'));
+ }
+
+
+ public function getUnreadCount()
+ {
+ /**
+ * @var $user User
+ */
+ $user = auth()->user();
+
+ /**
+ * @var $notification DatabaseNotification
+ */
+ $count = $user->unreadNotifications()->count();
+
+
+ $title = '';
+ $content = '';
+ $id = null;
+ if ($count > 0) {
+
+ $notification = $user->unreadNotifications()->first();
+
+ // 前端弹窗内容和标题相反显示,所以变量名会有点怪
+ $id = $notification->id;
+ $title = NotificationServe::getContent($notification);
+ $content = NotificationServe::getTitle($notification);
+ }
+
+ return responseJson(200, 'success', compact('count', 'title', 'content', 'id'));
+ }
+}
diff --git a/app/Http/Controllers/User/OrderController.php b/app/Http/Controllers/User/OrderController.php
new file mode 100644
index 0000000..0bbee4d
--- /dev/null
+++ b/app/Http/Controllers/User/OrderController.php
@@ -0,0 +1,326 @@
+getScoreRatio();
+
+ /**
+ * @var $user User
+ */
+ $user = auth()->user();
+ $query = $user->orders();
+
+ switch (request('tab', 0)) {
+
+ case 0:
+ break;
+ case 1:
+ // 待付款
+ $query->where('status', OrderStatusEnum::UN_PAY);
+ break;
+ case 2:
+ // 未发货
+ $query->where('status', OrderStatusEnum::PAID)->where('ship_status', OrderShipStatusEnum::PENDING);
+ break;
+ case 3:
+ // 待收货
+ $query->where('status', OrderStatusEnum::PAID)->where('ship_status', OrderShipStatusEnum::DELIVERED);
+ break;
+ case 4:
+ // 待评价
+ $query->where('status', OrderStatusEnum::PAID)->where('ship_status', OrderShipStatusEnum::RECEIVED);
+ break;
+ }
+
+ $orders = $query->latest()
+ ->with('details', 'details.product')
+ ->get()
+ ->map(
+ function (Order $order) use ($scoreRatio) {
+
+ // 可以或得到的积分
+ $order->score = ceil($order->amount*$scoreRatio);
+
+ // 完成按钮必须是已经支付和确认收货
+ $order->status_text = OrderStatusTransform::trans($order->status);
+ // 如果订单是付款了则显示发货状态
+ if ($order->status == OrderStatusEnum::PAID) {
+
+ // 如果发货了,则显示发货信息
+ $order->status_text = OrderShipStatusTransform::trans($order->ship_status);
+ }
+
+ $buttonServe = new OrderStatusButtonServe($order);
+ switch ($order->status) {
+ // 未支付的
+ case OrderStatusEnum::UN_PAY:
+
+ $buttonServe->payButton()->cancelOrderButton();
+ break;
+ case OrderStatusEnum::PAID:
+ // 已经确认收获了
+ if ($order->ship_status == OrderShipStatusEnum::RECEIVED) {
+
+ $buttonServe->completeButton();
+ } elseif ($order->ship_status == OrderShipStatusEnum::DELIVERED) {
+
+ $buttonServe->shipButton();
+ } else {
+
+ $buttonServe->refundButton();
+ }
+ break;
+
+ // 手动取消的订单
+ // 已经完成的订单
+ // 超时取消的订单
+ case OrderStatusEnum::UN_PAY_CANCEL:
+ case OrderStatusEnum::COMPLETED:
+ case OrderStatusEnum::TIMEOUT_CANCEL:
+ $buttonServe->replyBuyButton()->deleteButton();
+ break;
+ }
+
+ $order->buttons = $buttonServe->getButtons();
+
+ return $order;
+ }
+ );
+
+
+ // 查询订单总量
+ $unPayCount = $user->orders()->where('status', OrderStatusEnum::UN_PAY)->count();
+ $shipPendingCount = $user->orders()->where('status', OrderStatusEnum::PAID)->where('ship_status', OrderShipStatusEnum::PENDING)->count();
+ $shipDeliveredCount = $user->orders()->where('status', OrderStatusEnum::PAID)->where('ship_status', OrderShipStatusEnum::DELIVERED)->count();
+ $shipReceivedCount = $user->orders()->where('status', OrderStatusEnum::PAID)->where('ship_status', OrderShipStatusEnum::RECEIVED)->count();
+ $ordersCount = $user->orders()->count();
+
+ return view('user.orders.index', compact('orders', 'unPayCount', 'shipPendingCount', 'shipDeliveredCount', 'shipReceivedCount', 'ordersCount'));
+ }
+
+
+
+
+ public function show(Order $order)
+ {
+ if ($order->isNotUser(auth()->id())) {
+ abort(403, '你没有权限');
+ }
+
+ $order->ship_send = $order->ship_status == OrderShipStatusEnum::DELIVERED;
+ $order->confirm_ship = $order->ship_status == OrderShipStatusEnum::RECEIVED;
+
+ if ($order->confirm_ship) {
+
+ $order->ship_send = true;
+ }
+
+ $order->completed = $order->status == OrderShipStatusEnum::RECEIVED;
+
+ return view('user.orders.show', compact('order'));
+ }
+
+
+ public function completeOrder(Order $order, Request $request)
+ {
+ $star = intval($request->input('star'));
+ $content = $request->input('content');
+ if ($star < 0 || $star > 5) {
+
+ return responseJsonAsBadRequest('无效的评分');
+ }
+
+ if (empty($content)) {
+
+ return responseJsonAsBadRequest('请至少些一些内容吧');
+ }
+
+ // 判断是当前用户的订单才可以删除
+ $user = auth()->user();
+ if ($order->isNotUser($user->id)) {
+
+ return responseJsonAsBadRequest('你没有权限');
+ }
+
+ // 只有付完款的订单,而且必须是未完成的, 确认收货
+ if (
+ // 必须是已经付款,且已经确认收货的
+ ! ($order->status == OrderStatusEnum::PAID && $order->ship_status == OrderShipStatusEnum::RECEIVED)
+ ) {
+ return responseJsonAsBadRequest('订单当前状态不能完成');
+ }
+
+
+ $orderDetails = $order->details()->get();
+ $comments = $orderDetails->map(function (OrderDetail $orderDetail) use ($user, $content, $star) {
+
+ return [
+ 'order_id' => $orderDetail->order_id,
+ 'order_detail_id' => $orderDetail->id,
+ 'product_id' => $orderDetail->product_id,
+ 'user_id' => $user->id,
+ 'score' => $star,
+ 'content' => $content,
+ ];
+ });
+
+ DB::beginTransaction();
+
+ try {
+
+ // 订单完成
+ $order->status = OrderStatusEnum::COMPLETED;
+ $order->save();
+
+ // 评论内容
+ Comment::query()->insert($comments->all());
+
+ OrderDetail::query()->where('order_id', $order->id)->update(['is_commented' => true]);
+
+ // 完成订单增加积分
+ (new ScoreLogServe)->completeOrderAddScore($order);
+
+ DB::commit();
+ } catch (\Exception $e) {
+
+ return responseJsonAsServerError('服务器异常,请稍后再试');
+ }
+
+
+ return responseJson(200, '完成订单已增加积分');
+ }
+
+
+ public function confirmShip(Order $order)
+ {
+ // 判断是当前用户的订单才可以删除
+ if ($order->isNotUser(auth()->id())) {
+ abort(403, '你没有权限');
+ }
+
+ if ($order->status != OrderStatusEnum::PAID) {
+
+ return back()->withErrors('订单未付款');
+ }
+
+ if ($order->ship_status != OrderShipStatusEnum::DELIVERED) {
+
+ return back()->withErrors('订单未发货');
+ }
+
+ $order->ship_status = OrderShipStatusEnum::RECEIVED;
+ $order->save();
+
+ return back()->with('status', '收货成功');
+ }
+
+
+ public function cancelOrder(Order $order)
+ {
+ // 判断是当前用户的订单才可以删除
+ if ($order->isNotUser(auth()->id())) {
+ abort(403, '你没有权限');
+ }
+
+ if ($order->status != OrderStatusEnum::UN_PAY) {
+
+ return back()->withErrors('未付款的订单才能取消');
+ }
+
+
+
+ $pay = Pay::alipay(config('pay.ali'));
+
+ try {
+ $orderData = [
+ 'out_trade_no' => $order->no,
+ ];
+ $result = $pay->cancel($orderData);
+
+ $order->status = OrderStatusEnum::UN_PAY_CANCEL;
+ $order->save();
+
+ } catch (\Exception $e) {
+
+ return back()->withErrors('服务器异常,请稍后再试');
+ }
+
+
+ return back()->with('status', '取消成功');
+ }
+
+ /**
+ * 获取积分和钱的换比例
+ *
+ * @return mixed
+ */
+ protected function getScoreRatio()
+ {
+ $scoreRule = ScoreRule::query()->where('index_code', ScoreRuleIndexEnum::COMPLETE_ORDER)->firstOrFail();
+
+ return $scoreRule->score ?? 1;
+ }
+
+
+ /**
+ * 地址是否存在
+ *
+ * @param $address
+ * @return bool
+ */
+ protected function hasAddress($address)
+ {
+ return Address::query()
+ ->where('user_id', auth()->id())
+ ->where('id', $address)
+ ->exists();
+ }
+
+
+ public function destroy($id)
+ {
+ /**
+ * @var $order Order
+ */
+ $order = Order::query()->findOrFail($id);
+ // 判断是当前用户的订单才可以删除
+ if ($order->isNotUser(auth()->id())) {
+ abort(403, '你没有权限');
+ }
+
+ // 支付的订单不能删除
+ if (! in_array($order->status, [OrderStatusEnum::UN_PAY_CANCEL, OrderStatusEnum::TIMEOUT_CANCEL, OrderStatusEnum::COMPLETED])) {
+
+ abort(403, '订单不能删除');
+ }
+
+ $order->delete();
+
+ return back()->with('status', '删除成功');
+ }
+
+}
diff --git a/app/Http/Controllers/User/PaymentController.php b/app/Http/Controllers/User/PaymentController.php
new file mode 100644
index 0000000..522f56f
--- /dev/null
+++ b/app/Http/Controllers/User/PaymentController.php
@@ -0,0 +1,67 @@
+findOrFail($id);
+
+ // 生成支付信息
+ return $this->buildPayForm($masterOrder, (new Agent)->isMobile());
+ }
+
+ /**
+ * 生成支付订单
+ *
+ * @param Order $order
+ * @param $isMobile
+ * @return \Symfony\Component\HttpFoundation\Response
+ */
+ protected function buildPayForm(Order $order, $isMobile)
+ {
+ // 创建订单
+ $order = [
+ 'out_trade_no' => $order->no,
+ 'total_amount' => $order->amount,
+ 'subject' => $order->name,
+ ];
+
+ $pay = Pay::alipay(config('pay.ali'));
+
+ if ($isMobile) {
+
+ return $pay->wap($order);
+ }
+
+ return $pay->web($order);
+ }
+
+
+}
diff --git a/app/Http/Controllers/User/RefundController.php b/app/Http/Controllers/User/RefundController.php
new file mode 100644
index 0000000..f6542a6
--- /dev/null
+++ b/app/Http/Controllers/User/RefundController.php
@@ -0,0 +1,44 @@
+isNotUser(auth()->id())) {
+
+ return responseJson(403, '不是自己的订单');
+ }
+
+ // 订单必须是已经支付,而且还没有发货的
+ if ($order->status != OrderStatusEnum::PAID) {
+
+ return responseJson(403, '订单还没有付款');
+ }
+
+ if ($order->ship_status != OrderShipStatusEnum::PENDING) {
+
+ return responseJson(403, '订单已经发货');
+ }
+
+ // 保存退款理由
+ $order->status = OrderStatusEnum::APPLY_REFUND;
+ $order->refund_reason = $request->input('refund_reason');
+ $order->save();
+
+ return responseJson(200, '申请已提交,等待后台管理员审核');
+ }
+}
diff --git a/app/Http/Controllers/User/SeckillController.php b/app/Http/Controllers/User/SeckillController.php
new file mode 100644
index 0000000..c169083
--- /dev/null
+++ b/app/Http/Controllers/User/SeckillController.php
@@ -0,0 +1,235 @@
+getSeckill($seckill);
+
+ $product = $redisSeckill->product;
+
+ /**
+ * @var $user User
+ * 如果登录返回所有地址列表,如果没有,则返回一个空集合
+ */
+ $addresses = collect();
+ if ($user = auth()->user()) {
+
+ $addresses = $user->addresses()->get();
+ }
+
+ return view('seckills.show', compact('redisSeckill', 'product', 'addresses'));
+ }
+
+ /**
+ * 抢购秒杀
+ *
+ * @param Request $request
+ * @param $id
+ * @return \Illuminate\Http\JsonResponse
+ * @throws \Exception
+ */
+ public function storeSeckill(Request $request, $id)
+ {
+ /**
+ * 直接从 session 中读取 id,不经过数据库
+ *
+ * @var $user User
+ * @var $auth SessionGuard
+ */
+ $seckill = new Seckill(compact('id'));
+ $auth = auth('web');
+ $userId = session()->get($auth->getName());
+
+ try {
+
+ if (! $request->has('address_id')) {
+
+ throw new \Exception('必须选择一个地址');
+ }
+
+ // 验证是否有这个秒杀
+ // 验证秒杀活动是否已经结束
+ $redisSeckill = $this->redisSeckill = $this->getSeckill($seckill);
+
+ if (! $redisSeckill->is_start) {
+ throw new \Exception('秒杀未开始');
+ }
+
+ } catch (\Exception $e) {
+
+ return responseJson(402, $e->getMessage());
+ }
+
+// // 返回 0,代表之前已经设置过了,代表已经抢过
+// if (0 == Redis::hset($seckill->getUsersKey($userId), 'id', $userId)) {
+//
+// return responseJson(403, '你已经抢购过了');
+// }
+
+ // 开始抢购逻辑,如果从队列中读取不到了,代表已经抢购完成
+ if (is_null(Redis::lpop($seckill->getRedisQueueKey()))) {
+
+ return responseJson(403, '已经抢购完了');
+ }
+
+
+ DB::beginTransaction();
+
+ try {
+
+ $product = $redisSeckill->product;
+ if (is_null($product)) {
+ return responseJson(400, '商品已下架');
+ }
+
+ // 已经通过抢购请求,可以查询数据库
+ // 在这里验证一下地址是不是本人的
+ $user = auth()->user();
+ $address = Address::query()->where('user_id', $user->id)->find($request->input('address_id'));
+
+ if (is_null($address)) {
+ return responseJson(400, '无效的收货地址');
+ }
+
+ // 创建一个秒杀主表订单和明细表订单,默认数量一个
+ $masterOrder = ($orderUtil = new OrderUtil([['product' => $product]]))->make($user->id, $address);
+ $masterOrder->type = OrderTypeEnum::SEC_KILL;
+ $masterOrder->amount = $redisSeckill->price;
+ $masterOrder->save();
+
+ // 创建订单明细
+ $details = $orderUtil->getDetails();
+ data_set($details, '*.order_id', $masterOrder->id);
+ OrderDetail::query()->insert($details);
+
+
+ // 当订单超过三十分钟未付款,自动取消订单
+ $setting = new SettingKeyEnum(SettingKeyEnum::UN_PAY_CANCEL_TIME);
+ $delay = Carbon::now()->addMinute(setting($setting, 30));
+ CancelUnPayOrder::dispatch($masterOrder)->delay($delay);
+
+ // 生成支付信息
+ $form = $this->buildPayForm($masterOrder, (new Agent)->isMobile())->getContent();
+
+ } catch (\Exception $e) {
+
+ DB::rollBack();
+
+ // 回滚一个秒杀数量
+ Redis::lpush($seckill->getRedisQueueKey(), 9);
+ // 把当前用户踢出,给他继续抢购
+ Redis::del($seckill->getUsersKey($userId));
+
+ return responseJson(403, $e->getMessage());
+ }
+
+ DB::commit();
+
+ // 数量减 -
+ $redisSeckill->sale_count += 1;
+ $redisSeckill->number -= 1;
+ Redis::set($seckill->getRedisModelKey(), json_encode($redisSeckill));
+ // 存储抢购成功的用户名
+ $user = auth()->user();
+ Redis::hset($seckill->getUsersKey($userId), 'name', $user->hidden_name);
+
+
+
+ return responseJson(200, '抢购成功', compact('form'));
+ }
+
+ public function getSeckillUsers($id)
+ {
+ $seckill = new Seckill(compact('id'));
+ $keys = Redis::keys($seckill->getUsersKey('*'));
+
+ $users = collect();
+ foreach ($keys as $key) {
+
+ $users->push(Redis::hget($key, 'name'));
+ }
+
+ return responseJson(200, 'success', $users);
+ }
+
+ /**
+ * 从 redis 中获取秒杀的数据
+ *
+ * @param Seckill $seckill
+ * @return mixed
+ */
+ protected function getSeckill(Seckill $seckill)
+ {
+ /**
+ * @var $product Product
+ */
+ $json = Redis::get($seckill->getRedisModelKey());
+ $redisSeckill = json_decode($json);
+
+ if (is_null($redisSeckill)) {
+
+ abort(403, "没有这个秒杀活动");
+ }
+
+ // 得到这些时间
+ $now = Carbon::now();
+ $endAt = Carbon::make($redisSeckill->end_at);
+
+ if ($now->gt($endAt)) {
+
+ abort(403, "秒杀已经结束");
+ }
+
+ // 秒杀是否已经开始
+ $startAt = Carbon::make($redisSeckill->start_at);
+ $redisSeckill->is_start = $now->gt($startAt);
+ // 开始倒计时
+ $redisSeckill->diff_time = $startAt->getTimestamp() - time();
+
+ return $redisSeckill;
+ }
+
+ /**
+ * 重写父类的构建订单明细
+ *
+ * @param Product $product
+ * @param $number
+ * @return array
+ */
+ protected function buildOrderDetail(Product $product, $number)
+ {
+ $attribute = [
+ 'product_id' => $product->id,
+ 'number' => $number
+ ];
+
+ // 价格为秒杀的价格, 直接从 redis 中读取
+ $attribute['price'] = ceilTwoPrice($this->redisSeckill->price);
+ $attribute['total'] = ceilTwoPrice($attribute['price'] * $attribute['number']);
+
+ return $attribute;
+ }
+}
diff --git a/app/Http/Controllers/User/StoreOrderController.php b/app/Http/Controllers/User/StoreOrderController.php
new file mode 100644
index 0000000..66c2da4
--- /dev/null
+++ b/app/Http/Controllers/User/StoreOrderController.php
@@ -0,0 +1,218 @@
+user();
+
+
+ $cars = $request->input('cars', []);
+ $ids = $request->input('ids');
+ $numbers = $request->input('numbers');
+
+ if (count($ids) === 0 || count($ids) !== count($numbers)) {
+
+ return back()->with('error', '无效的商品');
+ }
+
+ $products = Product::query()->whereIn('uuid', $ids)->get();
+ if ($products->count() === 0 || $products->count() !== count($numbers)) {
+
+ return back()->with('error', '无效的商品');
+ }
+
+ $totalAmount = 0;
+ $products->transform(function (Product $product, $i) use ($numbers, &$totalAmount) {
+
+ $product->number = $numbers[$i];
+ $product->total_amount = round($product->price * $product->number);
+ $totalAmount += $product->total_amount;
+
+ return $product;
+ });
+
+ // 增加邮费
+ $postAmount = \setting(new SettingKeyEnum(SettingKeyEnum::POST_AMOUNT));
+ $totalAmount += $postAmount;
+
+ /**
+ * @var $user User
+ */
+ $user = auth()->user();
+ $addresses = $user->addresses()->latest()->get();
+
+ // 可用的优惠券
+ $today = Carbon::today()->toDateString();
+ $coupons = $user->coupons()
+ ->where('start_date', '<=', $today)
+ ->where('end_date', '>=', $today)
+ ->whereNull('used_at')
+ ->where('full_amount', '<=', $totalAmount)
+ ->latest()
+ ->get();
+
+ return view('orders.create', compact('products', 'cars', 'addresses', 'totalAmount', 'coupons', 'postAmount'));
+ }
+
+ public function store(Request $request)
+ {
+ if (($response = $this->validateRequest($request)) instanceof Response) {
+
+ return $response;
+ }
+
+
+ list($ids, $numbers, $productMap, $address, $couponModel) = $response;
+
+ // 构建出订单所需的详情
+ $detailsData = Collection::make($ids)->map(function ($id, $index) use ($numbers, $productMap) {
+
+ return [
+ 'number' => $numbers[$index],
+ 'product' => $productMap[$id]
+ ];
+ });
+
+
+ DB::beginTransaction();
+
+ try {
+
+
+ $masterOrder = ($orderUtil = new OrderUtil($detailsData))->make(auth()->id(), $address);
+
+ if (! is_null($couponModel) && $masterOrder->amount < $couponModel->full_amount) {
+
+ throw new \Exception('优惠券门槛金额为 ' . $couponModel->full_amount);
+ }
+
+ // 订单价格等于原价 - 优惠价格
+ if (! is_null($couponModel)) {
+
+ $masterOrder->amount = $masterOrder->amount > $couponModel->amount ?
+ ($masterOrder->amount - $couponModel->amount) : 0.01;
+
+ $couponModel->used_at = Carbon::now()->toDateTimeString();
+ $couponModel->save();
+
+ $masterOrder->coupon_id = $couponModel->id;
+ $masterOrder->coupon_amount = $couponModel->amount;
+ }
+
+ $masterOrder->save();
+
+ // 创建订单明细
+ $details = $orderUtil->getDetails();
+ data_set($details, '*.order_id', $masterOrder->id);
+ OrderDetail::query()->insert($details);
+
+ // 如果存在购物车,把购物车删除
+ $cars = $request->input('cars');
+ if (is_array($cars) && ! empty($cars)) {
+
+ Car::query()->where('user_id', auth()->id())->where('id', $cars)->delete();
+ }
+
+ // 当订单超过三十分钟未付款,自动取消订单
+ $settingKey = new SettingKeyEnum(SettingKeyEnum::UN_PAY_CANCEL_TIME);
+ $delay = Carbon::now()->addMinute(setting($settingKey, 30));
+ CancelUnPayOrder::dispatch($masterOrder)->delay($delay);
+
+ DB::commit();
+
+ } catch (\Exception $e) {
+
+ DB::rollBack();
+ return responseJsonAsBadRequest($e->getMessage());
+ }
+
+ return responseJson(200, '创建订单成功', ['order_id' => $masterOrder->id]);
+ }
+
+
+
+ private function validateRequest(Request $request)
+ {
+ /**
+ * @var $user User
+ * @var $address Address
+ */
+ $user = auth()->user();
+
+ $ids = $request->input('ids');
+ $numbers = $request->input('numbers');
+
+ if (count($ids) === 0 || count($ids) !== count($numbers)) {
+
+ return responseJsonAsBadRequest('无效的商品');
+ }
+
+ $productMap = Product::query()->whereIn('uuid', $ids)->get()->mapWithKeys(function (Product $product) {
+
+ return [$product->uuid => $product];
+ });
+ if (count($productMap) === 0 || count($productMap) !== count($numbers)) {
+
+ return responseJsonAsBadRequest('无效的商品.');
+ }
+
+
+ $address = $user->addresses()->find($request->input('address_id'));
+ if (is_null($address)) {
+
+ return responseJsonAsBadRequest('请选择收货地址');
+ }
+
+
+ // 查看是否有优惠券的价格
+ $couponModel = null;
+ if ($couponId = $request->input('coupon_id')) {
+
+ $couponModel = $user->coupons()->find($couponId);
+ if (is_null($couponModel)) {
+
+ return responseJsonAsBadRequest('无效的优惠券');
+ }
+
+ $today = Carbon::today();
+ $startDate = Carbon::make($couponModel->start_date) ?? Carbon::tomorrow();
+ $endDate = Carbon::make($couponModel->end_date) ?? Carbon::yesterday();
+ if ($today->lt($startDate) || $today->gt($endDate)) {
+
+ return responseJsonAsBadRequest('优惠券已过使用期');
+ }
+
+ if (! is_null($couponModel->used_at)) {
+
+ return responseJsonAsBadRequest('优惠券已使用过');
+ }
+ }
+
+ return [$ids, $numbers, $productMap, $address, $couponModel];
+ }
+}
diff --git a/app/Http/Controllers/User/UserController.php b/app/Http/Controllers/User/UserController.php
new file mode 100644
index 0000000..ecc5ce1
--- /dev/null
+++ b/app/Http/Controllers/User/UserController.php
@@ -0,0 +1,275 @@
+user();
+
+ // 获取优惠券数量
+ $today = Carbon::today()->toDateString();
+ $user->coupons_count = $user->coupons()->where('end_date', '>=', $today)->whereNull('used_at')->count();
+ $user->cars_count = $user->cars()->sum('number');
+ $user->orders_count = $user->orders()->count();
+ $user->likeProducts = $user->products()->latest()->take(9)->get();
+ $user->notifications_count = $user->unreadNotifications()->count();
+ $user->addresses_count = $user->addresses()->count();
+ $user->like_products_count = $user->products()->count();
+
+ // 查出用户的等级
+ $level = Level::query()
+ ->where('min_score', '<=', $user->score_all)
+ ->orderBy('min_score', 'desc')
+ ->first();
+
+ // 获取所有积分记录
+ $scoreLogs = $user->scoreLogs()->latest()->limit(5)->get();
+
+ return view('user.homes.index', compact('user', 'level', 'scoreLogs'));
+ }
+
+
+ public function indexScores()
+ {
+ /**
+ * @var $user User
+ */
+ $user = $this->user();
+
+ /**
+ * 显示所有可能的任务
+ *
+ * @var $rules Collection
+ */
+ $rules = Cache::rememberForever(ScoreRule::CACHE_KEY, function () {
+
+ return ScoreRule::query()
+ ->whereIn('index_code', ScoreRule::OPEN_RULES)
+ ->orderBy('index_code')
+ ->orderBy('score')
+ ->get();
+ });
+
+ // 连续登录, 浏览商品特殊处理
+ $loginDays = $user->login_days;
+ // 浏览的数量
+ $visitedNumber = (new ScoreLogServe)->getUserVisitedNumber(Carbon::today()->toDateString(), $user->id);
+
+ // 如果 completed_times === times 那么代表这个任务完成了
+ $rules->transform(function (ScoreRule $rule) use ($loginDays, $visitedNumber) {
+
+ if ($rule->index_code == ScoreRuleIndexEnum::CONTINUE_LOGIN) {
+
+ $rule->completed_times = $loginDays > $rule->times ? $rule->times : $loginDays;
+
+ } elseif ($rule->index_code == ScoreRuleIndexEnum::VISITED_PRODUCT) {
+
+ $rule->completed_times = $visitedNumber > $rule->times ? $rule->times : $visitedNumber;
+ }
+
+ $rule->plan = ($rule->completed_times / $rule->times) * 100;
+
+ return $rule;
+ });
+
+ $logs = $user->scoreLogs()->latest()->paginate(10);
+
+ return view('user.scores.index', compact('user', 'logs', 'rules'));
+ }
+
+
+ public function setting()
+ {
+ $user = $this->user();
+
+ return view('user.users.setting', compact('user'));
+ }
+
+
+ public function update(Request $request)
+ {
+ $user = $this->user();
+
+ $this->validate(
+ $request, [
+ 'avatar' => 'required',
+ 'sex' => 'in:0,1',
+ ], [
+ 'avatar.required' => '头像不能为空',
+ 'sex.in' => '性别格式不对',
+ ]
+ );
+
+ // 除了第三方授权登录的用户导致没有名字之外
+ // 其他用户是不允许修改用户名和邮箱
+ $user->sex = $request->input('sex');
+ $user->avatar = $request->input('avatar');
+
+
+ // 如果当前用户第一次修改用户名
+ if ($user->is_init_name && $request->filled('name')) {
+
+ $name = $request->input('name');
+
+ if (User::query()->where('name', $name)->exists()) {
+
+ return back()->withErrors('用户名已经存在');
+ }
+
+ $user->name = $name;
+ $user->is_init_name = false;
+ }
+
+ // 如果当前用户第一次修改邮箱
+ if ($user->is_init_email && $request->filled('email')) {
+
+ $email = $request->input('email');
+
+ if (User::query()->where('email', $email)->exists()) {
+
+ return back()->withErrors('邮箱已经存在');
+ }
+
+ $user->email = $email;
+ $user->is_init_email = false;
+ }
+
+ // 初始用户可以修改邮箱
+ $user->save();
+
+ return back()->with('status', '修改成功');
+ }
+
+
+ public function subscribe(Request $request)
+ {
+ $subscribeModel = $this->user()->subscribe()->firstOr(function () {
+
+ return new Subscribe();
+ });
+
+ $subscribeModel->email = $request->input('email');
+ // 如果已经存在了记录
+ if ($subscribeModel->exists) {
+
+ // 如果是已经有数据的, 代表已经订阅过了
+ $subscribeModel->is_subscribe = ! $subscribeModel->is_subscribe;
+ } else {
+ $subscribeModel->is_subscribe = 1;
+ $subscribeModel->user_id = $this->user()->id;
+ }
+ $subscribeModel->save();
+
+ if ($subscribeModel->is_subscribe) {
+ return responseJson(201, '订阅成功');
+ }
+
+ return responseJson(200, '欢迎下次再订阅');
+ }
+
+ /**
+ * 用户上传头像
+ *
+ * @param UploadServe $uploadServe
+ * @return array
+ */
+ public function uploadAvatar(UploadServe $uploadServe)
+ {
+ $disk = 'public';
+
+ try {
+ $link = $uploadServe->setFileInput('file')
+ ->setMaxSize('5M')
+ ->setExtensions(['jpg', 'jpeg', 'png', 'bmp', 'gif'])
+ ->validate()
+ ->store('avatars', compact('disk'));
+
+ } catch (UploadException $e) {
+
+ return [
+ 'code' => 302,
+ 'msg' => $e->getMessage(),
+ 'data' => []
+ ];
+ }
+
+
+ return [
+ 'code' => 0,
+ 'msg' => '图片上传成功',
+ 'data' => ['src' => $link, 'link' => \Storage::url($link)]
+ ];
+ }
+
+
+ public function showPasswordForm()
+ {
+ $user = $this->user();
+
+ return view('user.users.password', compact('user'));
+ }
+
+ public function updatePassword(Request $request)
+ {
+ $this->validate(
+ $request, [
+ 'password' => 'required|min:6|confirmed',
+ ], [
+ 'old_password.required' => '旧密码不能为空',
+ 'password.required' => '新密码不能为空',
+ 'password.min' => '新密码必须大于6位',
+ 'password.confirmed' => '两次密码不一致',
+ ]
+ );
+
+ $user = $request->user();
+ // 如果是从未设置过密码就就不用验证旧密码
+ if (! $user->is_init_password && ! $this->validatePassword($request->input('old_password'))) {
+ return back()->withErrors(['old_password' => '旧密码不正确']);
+ }
+
+ // 设置过密码之后,再也不是初始密码
+ $user->is_init_password = false;
+ $user->password = Hash::make($request->input('password'));
+ $user->save();
+
+ return back()->with('status', '密码修改成功');
+ }
+
+ private function validatePassword($oldPassword)
+ {
+ return Hash::check($oldPassword, $this->user()->password);
+ }
+
+ /**
+ * @return User|null
+ */
+ protected function user()
+ {
+ return auth()->user();
+ }
+
+
+}
diff --git a/app/Http/Controllers/User/UserCouponController.php b/app/Http/Controllers/User/UserCouponController.php
new file mode 100644
index 0000000..6444197
--- /dev/null
+++ b/app/Http/Controllers/User/UserCouponController.php
@@ -0,0 +1,128 @@
+user();
+
+ $query = $user->coupons();
+
+ switch ($request->input('tab', 1)) {
+
+ // 可使用的
+ case 1:
+ $today = Carbon::today()->toDateString();
+ $query->where('end_date', '>=', $today)->whereNull('used_at');
+ break;
+ // 已使用的
+ case 2:
+ $query->whereNotNull('used_at');
+ break;
+ // 未使用过期的
+ case 3:
+ $today = Carbon::today()->toDateString();
+ $query->whereNull('used_at')->where('end_date', '<', $today);
+ break;
+ }
+
+ $coupons = $query->latest()->paginate();
+
+ $today = Carbon::today();
+ foreach ($coupons as $coupon) {
+
+ if (! is_null($coupon->used_at)) {
+
+ $coupon->used = true;
+ $coupon->show_title = '已使用';
+ }
+ // 过期的
+ elseif ($today->gt(Carbon::make($coupon->end_date))) {
+
+ $coupon->used = true;
+ $coupon->show_title = '已过期';
+ } else {
+
+ $coupon->used = false;
+ $coupon->show_title = '去使用';
+ }
+
+ }
+
+ return view('user.coupons.index', compact('coupons'));
+ }
+
+
+ public function exchangeCoupon(Request $request)
+ {
+ $code = $request->input('code');
+ if (strlen($code) !== 16) {
+
+ return responseJsonAsBadRequest('兑换码必须是16位');
+ }
+
+ $codeModel = CouponCode::query()->where('code', $code)->first();
+ if (is_null($codeModel)) {
+
+ return responseJsonAsBadRequest('无效的兑换码');
+ }
+
+ $user = auth()->user();
+ if ($codeModel->user_id != $user->id) {
+
+ return responseJsonAsBadRequest('兑换码并非发放给你,请勿使用');
+ }
+
+ if (! is_null($codeModel->used_at)) {
+
+ return responseJsonAsBadRequest('兑换码已使用');
+ }
+
+ /**
+ * @var $template CouponTemplate
+ */
+ $template = $codeModel->template()->first();
+ if (is_null($template)) {
+
+ return responseJsonAsServerError('兑换码已过期');
+ }
+
+ $today = Carbon::today();
+ if ($today->gt(Carbon::make($template->end_date))) {
+
+ return responseJsonAsServerError('兑换码已过期');
+ }
+
+
+ $codeModel->used_at = Carbon::now()->toDateTimeString();
+ $codeModel->save();
+
+ // 开始领取优惠券
+ $coupon = new UserHasCoupon();
+ $coupon->template_id = $template->getKey();
+ $coupon->user_id = $user->id;
+
+ $coupon->title = $template->title;
+ $coupon->amount = $template->amount;
+ $coupon->full_amount = $template->full_amount;
+ $coupon->start_date = $template->start_date;
+ $coupon->end_date = $template->end_date;
+ $coupon->save();
+
+
+ return responseJson(200, "兑换{$template->title}成功");
+ }
+}
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
new file mode 100644
index 0000000..4635332
--- /dev/null
+++ b/app/Http/Kernel.php
@@ -0,0 +1,86 @@
+ [
+ \App\Http\Middleware\EncryptCookies::class,
+ \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
+ \Illuminate\Session\Middleware\StartSession::class,
+ \Illuminate\View\Middleware\ShareErrorsFromSession::class,
+ \App\Http\Middleware\VerifyCsrfToken::class,
+ \Illuminate\Routing\Middleware\SubstituteBindings::class,
+
+ // 用于记录用户连续登录天数的中间件
+ 'auth.login.score',
+ ],
+
+ 'api' => [
+ \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
+ \Illuminate\Routing\Middleware\SubstituteBindings::class,
+ ],
+ ];
+
+ /**
+ * The application's route middleware.
+ *
+ * These middleware may be assigned to groups or used individually.
+ *
+ * @var array
+ */
+ protected $routeMiddleware = [
+ 'auth' => \App\Http\Middleware\Authenticate::class,
+ 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
+ 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
+ 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
+ 'can' => \Illuminate\Auth\Middleware\Authorize::class,
+ 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
+ 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
+ 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
+ 'signed' => \App\Http\Middleware\ValidateSignature::class,
+ 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
+ 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
+ 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
+
+ // 用户登录验证
+ 'user.auth' => UserAuth::class,
+ // 用户购物车
+ 'user.cars' => ShareCarSum::class,
+
+ // api 刷新token
+ 'auth.api.refresh' => AuthRefreshToken::class,
+ 'auth.login.score' => RecordUserLoginDays::class,
+ ];
+}
diff --git a/app/Http/Middleware/AuthRefreshToken.php b/app/Http/Middleware/AuthRefreshToken.php
new file mode 100644
index 0000000..1752f5a
--- /dev/null
+++ b/app/Http/Middleware/AuthRefreshToken.php
@@ -0,0 +1,75 @@
+auth->parser()->hasToken()) {
+
+ return responseJsonAsUnAuthorized('未在请求中找到 token 验证信息');
+ }
+
+
+ // 设置过期时间
+ try {
+ /****************************************
+ * 尝试通过 tokne 登录,如果正常,就获取到用户
+ * 无法正确的登录,抛出 token 异常
+ ****************************************/
+ if ($this->auth->parseToken()->authenticate()) {
+
+ return $next($request);
+ }
+
+ return responseJsonAsNoFound('用户信息找不到');
+ }
+ // token 过期之后抛出的异常,尝试刷新 token
+ catch (TokenExpiredException $e) {
+
+ try {
+ /****************************************
+ * token 过期的异常,尝试刷新 token
+ * 使用 id 一次性登录以保证此次请求的成功
+ ****************************************/
+ $token = $this->auth->refresh();
+ $id = $this->auth->getPayload()->get('sub');
+ auth()->onceUsingId($id);
+
+ // 在响应头中返回新的 token
+ return $this->setAuthenticationHeader($next($request), $token);
+ }
+ // 已经无法刷新了,或者会被直接加入黑名单
+ catch (JWTException $e) {
+
+ /****************************************
+ * 如果捕获到此异常,即代表 refresh 也过期了,
+ * 用户无法刷新令牌,需要重新登录。
+ ****************************************/
+ return responseJsonAsAccountExpired('登录已过期,请重新登录');
+ }
+ }
+
+ // 更多的异常不会捕获, 直接再 Handler.php 处理
+ }
+}
diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php
new file mode 100644
index 0000000..d4ef644
--- /dev/null
+++ b/app/Http/Middleware/Authenticate.php
@@ -0,0 +1,17 @@
+expectsJson() ? null : route('login');
+ }
+}
diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php
new file mode 100644
index 0000000..867695b
--- /dev/null
+++ b/app/Http/Middleware/EncryptCookies.php
@@ -0,0 +1,17 @@
+
+ */
+ protected $except = [
+ //
+ ];
+}
diff --git a/app/Http/Middleware/PreventRequestsDuringMaintenance.php b/app/Http/Middleware/PreventRequestsDuringMaintenance.php
new file mode 100644
index 0000000..74cbd9a
--- /dev/null
+++ b/app/Http/Middleware/PreventRequestsDuringMaintenance.php
@@ -0,0 +1,17 @@
+
+ */
+ protected $except = [
+ //
+ ];
+}
diff --git a/app/Http/Middleware/RecordUserLoginDays.php b/app/Http/Middleware/RecordUserLoginDays.php
new file mode 100644
index 0000000..a9ba74b
--- /dev/null
+++ b/app/Http/Middleware/RecordUserLoginDays.php
@@ -0,0 +1,33 @@
+user();
+
+ if (! is_null($user)) {
+
+ (new ScoreLogServe)->loginAddScore($user);
+ }
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php
new file mode 100644
index 0000000..afc78c4
--- /dev/null
+++ b/app/Http/Middleware/RedirectIfAuthenticated.php
@@ -0,0 +1,30 @@
+check()) {
+ return redirect(RouteServiceProvider::HOME);
+ }
+ }
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/ShareCarSum.php b/app/Http/Middleware/ShareCarSum.php
new file mode 100644
index 0000000..b0ed5da
--- /dev/null
+++ b/app/Http/Middleware/ShareCarSum.php
@@ -0,0 +1,57 @@
+view = $view;
+ }
+
+ /**
+ * Handle an incoming request.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param \Closure $next
+ * @return mixed
+ */
+ public function handle($request, Closure $next)
+ {
+ // 如果用户已经登录,那么就是他的数量,如果用户没有登录,那么就是 0
+ $sum = 0;
+ /**
+ * @var $user User
+ */
+ if ($user = auth()->user()) {
+
+ $sum = $user->cars()->sum('number');
+ }
+
+
+ $this->view->share(
+ 'carSum', $sum
+ );
+
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php
new file mode 100644
index 0000000..88cadca
--- /dev/null
+++ b/app/Http/Middleware/TrimStrings.php
@@ -0,0 +1,19 @@
+
+ */
+ protected $except = [
+ 'current_password',
+ 'password',
+ 'password_confirmation',
+ ];
+}
diff --git a/app/Http/Middleware/TrustHosts.php b/app/Http/Middleware/TrustHosts.php
new file mode 100644
index 0000000..c9c58bd
--- /dev/null
+++ b/app/Http/Middleware/TrustHosts.php
@@ -0,0 +1,20 @@
+
+ */
+ public function hosts(): array
+ {
+ return [
+ $this->allSubdomainsOfApplicationUrl(),
+ ];
+ }
+}
diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php
new file mode 100644
index 0000000..3391630
--- /dev/null
+++ b/app/Http/Middleware/TrustProxies.php
@@ -0,0 +1,28 @@
+|string|null
+ */
+ protected $proxies;
+
+ /**
+ * The headers that should be used to detect proxies.
+ *
+ * @var int
+ */
+ protected $headers =
+ Request::HEADER_X_FORWARDED_FOR |
+ Request::HEADER_X_FORWARDED_HOST |
+ Request::HEADER_X_FORWARDED_PORT |
+ Request::HEADER_X_FORWARDED_PROTO |
+ Request::HEADER_X_FORWARDED_AWS_ELB;
+}
diff --git a/app/Http/Middleware/UserAuth.php b/app/Http/Middleware/UserAuth.php
new file mode 100644
index 0000000..a557dfb
--- /dev/null
+++ b/app/Http/Middleware/UserAuth.php
@@ -0,0 +1,28 @@
+check()) {
+
+ return redirect()->guest('login')->with('status', '请登录账号再操作');
+ }
+
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/ValidateSignature.php b/app/Http/Middleware/ValidateSignature.php
new file mode 100644
index 0000000..093bf64
--- /dev/null
+++ b/app/Http/Middleware/ValidateSignature.php
@@ -0,0 +1,22 @@
+
+ */
+ protected $except = [
+ // 'fbclid',
+ // 'utm_campaign',
+ // 'utm_content',
+ // 'utm_medium',
+ // 'utm_source',
+ // 'utm_term',
+ ];
+}
diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php
new file mode 100644
index 0000000..9e86521
--- /dev/null
+++ b/app/Http/Middleware/VerifyCsrfToken.php
@@ -0,0 +1,17 @@
+
+ */
+ protected $except = [
+ //
+ ];
+}
diff --git a/app/Http/Requests/AddressRequest.php b/app/Http/Requests/AddressRequest.php
new file mode 100644
index 0000000..bdde90c
--- /dev/null
+++ b/app/Http/Requests/AddressRequest.php
@@ -0,0 +1,45 @@
+ 'required',
+ 'phone'=>'regex:/^1[34578][0-9]{9}$/',
+ 'province_id' => 'required',
+ 'city_id' => 'required',
+ 'detail_address' => 'required',
+ ];
+ }
+
+ public function messages()
+ {
+ return [
+ 'name.required' => '收货人名字不能为空',
+ 'phone.regex' => '手机号码格式不正确',
+ 'province_id.required' => '省区不能为空',
+ 'city_id.required' => '城市不能为空',
+ 'detail_address.required' => '详细收货地址不能为空',
+ ];
+ }
+}
diff --git a/app/Http/Requests/AdminRequest.php b/app/Http/Requests/AdminRequest.php
new file mode 100644
index 0000000..05ccad3
--- /dev/null
+++ b/app/Http/Requests/AdminRequest.php
@@ -0,0 +1,52 @@
+ 'required|unique:admins',
+ 'password' => 'required|min:6|confirmed',
+ 'roles' => 'required|array'
+ ];
+
+ if ($this->method() == 'PUT') {
+ $rules['name'] = 'required';
+ $rules['password'] = '';
+ }
+
+ return $rules;
+ }
+
+ public function messages()
+ {
+ return [
+ 'name.required' => '管理员名字不能为空',
+ 'name.unique' => '管理员名字已经存在',
+ 'password.required' => '密码不能为空',
+ 'password.min' => '密码最少六位',
+ 'password.confirmed' => '两次密码不相同',
+ 'roles.required' => '角色不能为空',
+ 'roles.array' => '角色不符合规范',
+ ];
+ }
+}
diff --git a/app/Http/Requests/CategoryRequest.php b/app/Http/Requests/CategoryRequest.php
new file mode 100644
index 0000000..6e29644
--- /dev/null
+++ b/app/Http/Requests/CategoryRequest.php
@@ -0,0 +1,53 @@
+ 'required|unique:categories',
+ 'thumb' => 'required',
+ 'parent_id' => Rule::notIn(['-1']),
+ 'description' => 'required|min:10',
+ ];
+
+ if ($this->method() == 'PUT') {
+ $rules['name'] = 'required';
+ $rules['thumb'] = '';
+ }
+
+ return $rules;
+ }
+
+ public function messages()
+ {
+ return [
+ 'name.required' => '分类名称不能为空',
+ 'name.unique' => '分类名已经存在',
+ 'thumb.required' => '请选择分类缩略图',
+ 'parent_id.not_in' => '请选择一个分类',
+ 'description.required' => '分类描述不能为空',
+ 'description.min' => '分类描述不能少于10个字',
+ ];
+ }
+}
diff --git a/app/Http/Requests/LoginRequest.php b/app/Http/Requests/LoginRequest.php
new file mode 100644
index 0000000..d2ada4d
--- /dev/null
+++ b/app/Http/Requests/LoginRequest.php
@@ -0,0 +1,31 @@
+ 'required',
+ 'password' => 'required',
+ ];
+ }
+}
diff --git a/app/Http/Requests/PermissionRequest.php b/app/Http/Requests/PermissionRequest.php
new file mode 100644
index 0000000..63169f3
--- /dev/null
+++ b/app/Http/Requests/PermissionRequest.php
@@ -0,0 +1,44 @@
+ 'required|unique:permissions,name'
+ ];
+
+ if ($this->method() == 'PUT') {
+ $rules['name'] = 'required';
+ }
+
+ return $rules;
+ }
+
+ public function messages()
+ {
+ return [
+ 'name.required' => '权限名称不能为空',
+ 'name.unique' => '权限已存在'
+ ];
+ }
+}
diff --git a/app/Http/Requests/ProductRequest.php b/app/Http/Requests/ProductRequest.php
new file mode 100644
index 0000000..e84e784
--- /dev/null
+++ b/app/Http/Requests/ProductRequest.php
@@ -0,0 +1,95 @@
+ "required|exists:categories,id",
+ "name" => "required|unique:products",
+ "title" => "required|max:50",
+ "price" => "required|numeric",
+ "original_price" => "required|numeric",
+
+ // product_details table field
+ "unit" => 'required',
+ "count" => 'required|numeric',
+ "description" => "required|min:10",
+
+
+ // attribute table field
+ "attribute" => 'required|array',
+ "attribute.*" => 'required',
+ "items" => 'required|array',
+ "items.*" => 'required',
+ "markup" => 'required|array',
+ "markup.*" => 'required|numeric',
+
+ // product_images table field
+ "link" => 'required|array',
+ ];
+
+ if ($this->method() == 'PUT') {
+ $rules['name'] = 'required';
+ }
+
+
+ return $rules;
+ }
+
+ public function messages()
+ {
+ return [
+ "category_id.required" => "请选择商品分裂",
+ "category_id.exists" => "请选择一个正确的分类",
+ "name.required" => "商品名字不能为空",
+ "name.unique" => "商品名字已经存在",
+ "title.required" => "商品描述不能为空",
+ "title.max" => "商品描述不能大于50个字",
+ "price.required" => "商品销售价格不能为空",
+ "price.numeric" => "商品销售价格必须是数字",
+ "original_price.required" => "商品展示价格不能为空",
+ "original_price.numeric" => "商品展示价格必须是数字",
+
+ "unit.required" => '商品计数单位不能为空',
+ "count.required" => '商品库存量不能为空',
+ "count.numeric" => '商品库存量必须是数字',
+ "description.required" => "商品详情不能为空",
+ "description.min" => "商品详情请在10个字以上",
+
+ "attribute.required" => '商品的属性不能为空',
+ "attribute.array" => '商品的属性不符合规格',
+ "items.required" => '商品的属性的值不能为空',
+ "items.array" => '商品的属性的值不符合规格',
+ "markup.required" => '价格浮动不能为空',
+ "markup.array" => '价格浮动不符合规格',
+ "attribute.*.required" => '商品属性不能为空',
+ "items.*.required" => '商品属性的值不能为空',
+ "markup.*.required" => '商品价格浮动不能为空',
+ "markup.*.numeric" => '商品价格浮动必须是数字',
+
+ "link.required" => '必须上传商品图片',
+ "link.array" => '商品图片不符合规格',
+ ];
+ }
+}
diff --git a/app/Http/Requests/RegisterUserRequest.php b/app/Http/Requests/RegisterUserRequest.php
new file mode 100644
index 0000000..bf43ec8
--- /dev/null
+++ b/app/Http/Requests/RegisterUserRequest.php
@@ -0,0 +1,31 @@
+ 'required',
+ 'password' => 'required',
+ ];
+ }
+}
diff --git a/app/Http/Requests/RoleRequest.php b/app/Http/Requests/RoleRequest.php
new file mode 100644
index 0000000..ac408e2
--- /dev/null
+++ b/app/Http/Requests/RoleRequest.php
@@ -0,0 +1,47 @@
+ 'required|unique:roles,name',
+ 'permission' => 'required|array'
+ ];
+
+ if ($this->method() == 'PUT') {
+ $rules['name'] = 'required';
+ }
+
+ return $rules;
+ }
+
+ public function messages()
+ {
+ return [
+ 'name.required' => '角色名不能为空',
+ 'name.unique' => '角色名已经存在',
+ 'permission.required' => '权限不能为空',
+ 'permission.array' => '权限格式错误',
+ ];
+ }
+}
diff --git a/app/Http/Requests/StoreAdminPost.php b/app/Http/Requests/StoreAdminPost.php
new file mode 100644
index 0000000..ef55f0d
--- /dev/null
+++ b/app/Http/Requests/StoreAdminPost.php
@@ -0,0 +1,44 @@
+ 'required|min:3',
+ 'password' => 'required|min:5',
+ 'captcha' => 'required|captcha',
+ ];
+ }
+
+ public function messages()
+ {
+ return [
+ 'name.required' => '用户名不能为空',
+ 'name.min' => '用户名不能少于五位',
+ 'password.required' => '密码不能少于五位',
+ 'password.min' => '密码不能少于五位',
+ 'captcha.required' => '验证码不能为空',
+ 'captcha.captcha' => '验证码不正确',
+ ];
+ }
+}
diff --git a/app/Http/Requests/StoreSeckillRequest.php b/app/Http/Requests/StoreSeckillRequest.php
new file mode 100644
index 0000000..56d672e
--- /dev/null
+++ b/app/Http/Requests/StoreSeckillRequest.php
@@ -0,0 +1,30 @@
+ 'required|exists:categories,id',
+ 'product_id' => 'required|exists:products,id',
+ 'number' => 'required|integer|min:1',
+ 'start_at' => 'required|date',
+ 'end_at' => 'required|date|after_or_equal:start_at',
+ ];
+ }
+}
diff --git a/app/Http/Resources/CategoreResource.php b/app/Http/Resources/CategoreResource.php
new file mode 100644
index 0000000..288a54d
--- /dev/null
+++ b/app/Http/Resources/CategoreResource.php
@@ -0,0 +1,25 @@
+ $this->id,
+ 'name' => (string)$this->title,
+ 'description' => (string)$this->description,
+ 'icon' => (string)$this->icon,
+ 'thumb' => assertUrl((string)$this->thumb),
+ ];
+ }
+}
diff --git a/app/Http/Resources/OwnResource.php b/app/Http/Resources/OwnResource.php
new file mode 100644
index 0000000..0fa0d8b
--- /dev/null
+++ b/app/Http/Resources/OwnResource.php
@@ -0,0 +1,37 @@
+ $this->name,
+ 'sex' => $this->sex,
+ 'email' => (string)$this->email,
+ 'avatar' => (string)$this->avatar,
+ 'github_name' => (string)$this->github_name,
+ 'qq_name' => (string)$this->qq_name,
+ 'weibo_name' => (string)$this->weibo_name,
+
+ 'score_all' => (int)$this->score_all,
+ 'score_now' => (int)$this->score_now,
+
+ 'login_days' => (int)$this->login_days,
+ 'last_login_date' => (string)$this->last_login_date,
+
+ 'is_init_name' => (bool)$this->is_init_name,
+ 'is_init_email' => (bool)$this->is_init_email,
+ 'is_init_password' => (bool)$this->is_init_password,
+ ];
+ }
+}
diff --git a/app/Http/Resources/ProductResource.php b/app/Http/Resources/ProductResource.php
new file mode 100644
index 0000000..6dc8017
--- /dev/null
+++ b/app/Http/Resources/ProductResource.php
@@ -0,0 +1,47 @@
+ (string)$this->uuid,
+ 'name' => (string)$this->name,
+ 'title' => (string)$this->title,
+ 'price' => (double)$this->price,
+ 'original_price' => (double)$this->original_price,
+ 'thumb' => $this->thumb,
+ 'sale_count' => (int)$this->sale_count,
+ 'count' => (int)$this->count,
+ 'view_count' => (int)$this->view_count,
+ 'created_at' => (string)$this->created_at,
+
+ 'content' => $this->whenLoaded('detail', function () {
+
+ return (string)optional($this->detail)->content;
+ }),
+ 'pictures' => $this->whenLoaded('detail', function () {
+
+ return $this->assertPictures($this->pictures);
+ }),
+ ];
+ }
+
+ protected function assertPictures($pictures)
+ {
+ return collect($pictures)->map(function ($uri) {
+
+ return assertUrl($uri);
+ });
+ }
+}
diff --git a/app/Http/Resources/ScoreLogResource.php b/app/Http/Resources/ScoreLogResource.php
new file mode 100644
index 0000000..62cf0eb
--- /dev/null
+++ b/app/Http/Resources/ScoreLogResource.php
@@ -0,0 +1,24 @@
+ $this->id,
+ 'description' => $this->description,
+ 'score' => $this->score,
+ 'created_at' => (string)$this->created_at
+ ];
+ }
+}
diff --git a/app/Jobs/CancelUnPayOrder.php b/app/Jobs/CancelUnPayOrder.php
new file mode 100644
index 0000000..e56e750
--- /dev/null
+++ b/app/Jobs/CancelUnPayOrder.php
@@ -0,0 +1,72 @@
+order = $order;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ // 查询数据库最新状态
+ $nowOrder = $this->order->refresh();
+
+ // 如果现在还是没有付款,那么则取消订单
+ if ($nowOrder->status == OrderStatusEnum::UN_PAY) {
+
+ // 如果订单还是没有付款
+ // 未付款设置为取消状态,
+ $this->order->status = OrderStatusEnum::UN_PAY_CANCEL;
+ $this->order->save();
+
+ // 回退优惠券
+ if (
+ !is_null($this->order->coupon_id) &&
+ $coupon = UserHasCoupon::query()->find($this->order->coupon_id)
+ ) {
+
+ $coupon->used_at = null;
+ $coupon->save();
+ }
+
+ // 回滚库存
+ $this->order
+ ->details()
+ ->with('product')
+ ->get()
+ ->map(function (OrderDetail $detail) {
+
+ // 不回滚出售数量
+ $product = $detail->product;
+ $product->increment('count', $detail->number);
+ });
+ }
+ }
+}
diff --git a/app/Jobs/InstallShopWarn.php b/app/Jobs/InstallShopWarn.php
new file mode 100644
index 0000000..29737f8
--- /dev/null
+++ b/app/Jobs/InstallShopWarn.php
@@ -0,0 +1,42 @@
+user = $user;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $time = date('Y-m-d H:i:s');
+ $msg = '网站已启动|启动时间: ' . $time;
+ Log::info($msg);
+ }
+}
diff --git a/app/Jobs/RemindUsersHasSeckill.php b/app/Jobs/RemindUsersHasSeckill.php
new file mode 100644
index 0000000..47f2a79
--- /dev/null
+++ b/app/Jobs/RemindUsersHasSeckill.php
@@ -0,0 +1,54 @@
+seckill = $seckill;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ /**
+ * @var $product Product
+ */
+ // 拿出收藏的用户
+ $product = $this->seckill->product;
+
+ $product->users()
+ ->get()
+ ->map(function (User $user) use ($product) {
+
+ Mail::to($user->email)->send(new RemindUserHasSeckillEmail($this->seckill, $product));
+ });
+
+
+ }
+}
diff --git a/app/Listeners/EventListener.php b/app/Listeners/EventListener.php
new file mode 100644
index 0000000..dbf3337
--- /dev/null
+++ b/app/Listeners/EventListener.php
@@ -0,0 +1,31 @@
+product = $product;
+ $this->seckill = $seckill;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ return $this->markdown('emails.seckills');
+ }
+}
diff --git a/app/Mail/ResetPassword.php b/app/Mail/ResetPassword.php
new file mode 100644
index 0000000..9257c5e
--- /dev/null
+++ b/app/Mail/ResetPassword.php
@@ -0,0 +1,39 @@
+token = $token;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ $url = url('password/reset', $this->token);
+
+ return $this->view('emails.password.reset', compact('url'));
+ }
+}
diff --git a/app/Mail/SubscribesNotice.php b/app/Mail/SubscribesNotice.php
new file mode 100644
index 0000000..8b56b25
--- /dev/null
+++ b/app/Mail/SubscribesNotice.php
@@ -0,0 +1,28 @@
+unSubUrl = $url;
+ }
+
+
+ public function build()
+ {
+ $latest = Product::query()->latest()->first();
+ $hottest = Product::query()->orderBy('sale_count', 'desc')->first();
+ $likest = Product::query()->withCount('users')->orderBy('users_count', 'desc')->first();
+
+ return $this->markdown('emails.subscribes', compact('likest', 'latest', 'hottest'));
+ }
+}
diff --git a/app/Mail/UserRegister.php b/app/Mail/UserRegister.php
new file mode 100644
index 0000000..c81a085
--- /dev/null
+++ b/app/Mail/UserRegister.php
@@ -0,0 +1,38 @@
+user = $user;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ return $this->view('emails.register');
+ }
+}
diff --git a/app/Models/Address.php b/app/Models/Address.php
new file mode 100644
index 0000000..c53519d
--- /dev/null
+++ b/app/Models/Address.php
@@ -0,0 +1,68 @@
+belongsTo(User::class);
+ }
+
+ public function orders()
+ {
+ return $this->hasMany(Order::class);
+ }
+
+ public function format()
+ {
+ return optional($this->province)->name . optional($this->city)->name. $this->detail_address;
+ }
+
+ public function province()
+ {
+ return $this->belongsTo(Province::class);
+ }
+
+ public function city()
+ {
+ return $this->belongsTo(City::class);
+ }
+}
diff --git a/app/Models/AdminUser.php b/app/Models/AdminUser.php
new file mode 100644
index 0000000..9879898
--- /dev/null
+++ b/app/Models/AdminUser.php
@@ -0,0 +1,32 @@
+environment('dev')) {
+
+ throw new \Exception('开发环境不允许操作');
+ }
+ });
+
+
+ self::deleting(function () {
+
+ if (app()->environment('dev')) {
+
+ throw new \Exception('开发环境不允许操作');
+ }
+ });
+ }
+}
diff --git a/app/Models/ArticleNotification.php b/app/Models/ArticleNotification.php
new file mode 100644
index 0000000..0ba4017
--- /dev/null
+++ b/app/Models/ArticleNotification.php
@@ -0,0 +1,48 @@
+get();
+
+ $now = Carbon::now();
+ $notifications = $users->map(function (User $user) use ($now, $article) {
+
+ $notification = [
+ 'id' => Uuid::uuid4()->toString(),
+ 'type' => ArticleTitleNotification::class,
+ 'notifiable_id' => $user->id,
+ 'notifiable_type' => get_class($user),
+ 'data' => json_encode((new ArticleTitleNotification($article))->toArray($user), JSON_UNESCAPED_UNICODE),
+ 'created_at' => $now,
+ 'updated_at' => $now,
+ ];
+ return $notification;
+ });
+
+ $size = 1000;
+ foreach (array_chunk($notifications->all(), $size, true) as $chunk) {
+ // 通知
+ DatabaseNotification::query()->insert($chunk);
+ }
+ });
+ }
+}
diff --git a/app/Models/Car.php b/app/Models/Car.php
new file mode 100644
index 0000000..539d014
--- /dev/null
+++ b/app/Models/Car.php
@@ -0,0 +1,49 @@
+belongsTo(User::class);
+ }
+
+ public function product()
+ {
+ return $this->belongsTo(Product::class)->withDefault(function () {
+
+ $product = new Product();
+ $product->name = '商品已下架';
+ $product->thumb = get404Image();
+ $product->price = 0;
+ });
+ }
+}
diff --git a/app/Models/Category.php b/app/Models/Category.php
new file mode 100644
index 0000000..b1a2c73
--- /dev/null
+++ b/app/Models/Category.php
@@ -0,0 +1,63 @@
+hasMany(Product::class);
+ }
+
+
+ public static function orderAll()
+ {
+ return self::query()->orderBy('order')->latest()->get();
+ }
+
+ public static function selectOrderAll()
+ {
+ return self::query()->orderBy('order')->latest()->pluck('title', 'id');
+ }
+}
diff --git a/app/Models/City.php b/app/Models/City.php
new file mode 100644
index 0000000..8724c34
--- /dev/null
+++ b/app/Models/City.php
@@ -0,0 +1,24 @@
+belongsTo(Product::class);
+ }
+
+ public function user()
+ {
+ return $this->belongsTo(User::class)->withDefault(function () {
+
+ return new User(['avatar' => 'avatars/default/' . array_random(User::DEFAULT_AVATARS)]);
+ });
+ }
+}
diff --git a/app/Models/CouponCode.php b/app/Models/CouponCode.php
new file mode 100644
index 0000000..bee0bd0
--- /dev/null
+++ b/app/Models/CouponCode.php
@@ -0,0 +1,18 @@
+belongsTo(User::class, 'user_id');
+ }
+
+ public function template()
+ {
+ return $this->belongsTo(CouponTemplate::class, 'template_id');
+ }
+}
diff --git a/app/Models/CouponTemplate.php b/app/Models/CouponTemplate.php
new file mode 100644
index 0000000..850ec85
--- /dev/null
+++ b/app/Models/CouponTemplate.php
@@ -0,0 +1,60 @@
+hasMany(UserHasCoupon::class, 'template_id');
+ }
+
+ public function getAmountAttribute($value)
+ {
+ if ($value == intval($value)) {
+
+ $value = intval($value);
+ }
+
+ return $value;
+ }
+
+ public function getFullAmountAttribute($value)
+ {
+ if ($value == intval($value)) {
+
+ $value = intval($value);
+ }
+
+ return $value;
+ }
+}
diff --git a/app/Models/Level.php b/app/Models/Level.php
new file mode 100644
index 0000000..38ee769
--- /dev/null
+++ b/app/Models/Level.php
@@ -0,0 +1,34 @@
+hasMany(OrderDetail::class);
+ }
+
+ public function address()
+ {
+ return $this->belongsTo(Address::class);
+ }
+
+ public function user()
+ {
+ return $this->belongsTo(User::class);
+ }
+
+ public function isNotUser($id)
+ {
+ return $this->user_id != $id;
+ }
+
+ public static function boot()
+ {
+ parent::boot();
+
+
+ // 自动生成订单的订单号
+ static::creating(function ($model) {
+
+ if (is_null($model->no)) {
+ $model->no = static::findAvailableNo($model->user_id);
+ }
+ });
+
+ static::created(function ($model) {
+
+ // 订单成交量
+ Cache::increment(SiteCountCacheEnum::ORDER_COUNT);
+ });
+
+ static::saved(function ($model) {
+
+ // 支付
+ if ($model->status == OrderStatusEnum::PAID) {
+ // 订单成交量
+ Cache::increment(SiteCountCacheEnum::PAY_ORDER_COUNT);
+
+ $currMoney = Cache::get(SiteCountCacheEnum::SALE_ORDER_COUNT, 0);
+ if (function_exists('bcadd')) {
+ $money = bcadd($currMoney, $model->pay_amount);
+ } else {
+ $money = $currMoney + $model->pay_amount;
+ }
+
+ Cache::set(SiteCountCacheEnum::SALE_ORDER_COUNT, $money);
+ }
+ // 退款
+ elseif ($model->status == OrderStatusEnum::REFUND) {
+
+ $currMoney = Cache::get(SiteCountCacheEnum::SALE_ORDER_COUNT, 0);
+ if (function_exists('bcsub')) {
+ $money = bcsub($currMoney, $model->pay_refund_fee);
+ } else {
+ $money = $currMoney - $model->pay_refund_fee;
+ }
+
+ Cache::increment(SiteCountCacheEnum::REFUND_ORDER_COUNT);
+ Cache::set(SiteCountCacheEnum::SALE_ORDER_COUNT, $money);
+ }
+
+ });
+ }
+
+ /**
+ * @param string $userId
+ * @param int $try
+ * @return string
+ * @throws \Exception
+ */
+ public static function findAvailableNo($userId = '000000000', $try = 5)
+ {
+ $prefix = date('YmdHis');
+ $suffix = fixStrLength($userId, 9);
+
+ for ($i = 0; $i < $try; ++ $i) {
+ $no = $prefix . fixStrLength(random_int(0, 9999), 5) . $suffix;
+
+ if (self::query()->where('no', $no)->doesntExist()) {
+ return $no;
+ }
+ }
+
+ throw new \Exception('流水号生成失败');
+ }
+}
diff --git a/app/Models/OrderDetail.php b/app/Models/OrderDetail.php
new file mode 100644
index 0000000..5f7f9ac
--- /dev/null
+++ b/app/Models/OrderDetail.php
@@ -0,0 +1,63 @@
+belongsTo(Product::class)->withDefault([
+ 'name' => '商品已下架',
+ 'thumb' => assertUrl('products/404.jpg')
+ ]);
+ }
+
+ public function order()
+ {
+ return $this->belongsTo(Order::class);
+ }
+
+ public function comment()
+ {
+ return $this->hasOne(Comment::class);
+ }
+}
diff --git a/app/Models/Product.php b/app/Models/Product.php
new file mode 100644
index 0000000..70f78b0
--- /dev/null
+++ b/app/Models/Product.php
@@ -0,0 +1,260 @@
+ 'json',
+ ];
+
+
+
+ public function getThumbAttribute($thumb)
+ {
+ return assertUrl($thumb);
+ }
+
+ public function comments()
+ {
+ return $this->hasMany(Comment::class);
+ }
+
+ public function category()
+ {
+ return $this->belongsTo(Category::class);
+ }
+
+ public function cars()
+ {
+ return $this->hasMany(Car::class);
+ }
+
+ public function users()
+ {
+ return $this->belongsToMany(User::class, 'likes_products')->withTimestamps();
+ }
+
+ public function detail()
+ {
+ return $this->hasOne(ProductDetail::class);
+ }
+
+
+ public function orderDetail()
+ {
+ return $this->hasOne(orderDetail::class);
+ }
+
+ /**
+ * 使用 uuid 注入
+ *
+ * @return string
+ */
+ public function getRouteKeyName()
+ {
+ return 'uuid';
+ }
+
+ public function getViewCountAttribute()
+ {
+ $date = Carbon::today()->toDateString();
+
+ return $this->attributes['view_count'] + Cache::get($this->getViewCountKey($date), 0);
+ }
+
+ public function getViewCountKey($date)
+ {
+ return "moon:products_cache_{$date}:view_count_{$this->id}";
+ }
+
+ public static function boot()
+ {
+ parent::boot();
+
+
+ // 自动生成商品的 uuid, 拼音
+ static::saving(function (Product $model) {
+
+ if (is_null($model->uuid)) {
+ $model->uuid = Uuid::uuid4()->toString();
+ }
+
+ if (is_null($model->pinyin)) {
+
+ /**
+ * @var $pinyin Pinyin
+ */
+ $pinyin = app(Pinyin::class);
+
+ $model->pinyin = $pinyin->permalink($model->name);
+ $model->first_pinyin = substr($model->pinyin, 0, 1);
+ }
+
+
+ if ($model->isDirty('first_pinyin')) {
+
+ // 建立拼音表
+ ProductPinYin::query()->firstOrCreate(['pinyin' => $model->first_pinyin]);
+ }
+
+ if (self::$addToSearch) {
+ try {
+
+ $model->addToIndex($model->getSearchData());
+ } catch (\Exception $e) {
+
+ }
+ }
+
+ });
+
+ static::deleted(function (Product $model) {
+
+ // 没有这个拼音了,删去
+ if (Product::query()->where('first_pinyin', $model->first_pinyin)->doesntExist()) {
+ ProductPinYin::query()->where('pinyin', $model->first_pinyin)->delete();
+ }
+
+ if (self::$addToSearch) {
+ try {
+
+ $model->removeFromIndex();
+ } catch (\Exception $e) {
+
+ }
+ }
+ });
+
+
+ // 从软删除中恢复
+ static::restored(function (Product $model) {
+
+ // 建立拼音表
+ ProductPinYin::query()->firstOrCreate(['pinyin' => $model->first_pinyin]);
+
+ if (self::$addToSearch) {
+ try {
+
+ $model->addToIndex($this->getSearchData());
+ } catch (\Exception $e) {
+
+ }
+ }
+ });
+ }
+
+ public function getSearchData()
+ {
+ $categoryName = $this->category->title ?? '';
+ $title = $this->name . ' ' . $this->title;
+ $text = str_replace(["\t", "\r", "\n"], ['', '', ''], strip_tags($this->detail->content ?? ''));
+
+ return [
+ 'id' => $this->id,
+ 'title' => $title,
+ 'body' => $text . ' ' . $categoryName
+ ];
+ }
+
+
+ public function getIndexName()
+ {
+ return 'product';
+ }
+
+ public function getMappingProperties()
+ {
+ return [
+ 'id' => [
+ 'type' => 'integer'
+ ],
+ 'title' => [
+ 'type' => 'text',
+ 'analyzer' => 'ik_max_word',
+ 'search_analyzer' => 'ik_smart',
+ 'index' => true,
+ ],
+ 'body' => [
+ 'type' => 'text',
+ 'analyzer' => 'ik_max_word',
+ 'search_analyzer' => 'ik_smart',
+ 'index' => true,
+ ]
+ ];
+ }
+}
diff --git a/app/Models/ProductDetail.php b/app/Models/ProductDetail.php
new file mode 100644
index 0000000..ab32720
--- /dev/null
+++ b/app/Models/ProductDetail.php
@@ -0,0 +1,36 @@
+belongsTo(Product::class);
+ }
+}
diff --git a/app/Models/ProductHasUser.php b/app/Models/ProductHasUser.php
new file mode 100644
index 0000000..1cf652e
--- /dev/null
+++ b/app/Models/ProductHasUser.php
@@ -0,0 +1,38 @@
+belongsTo(Product::class, 'product_id');
+ }
+
+ public function user()
+ {
+ return $this->belongsTo(User::class, 'user_id');
+ }
+}
diff --git a/app/Models/ProductPinYin.php b/app/Models/ProductPinYin.php
new file mode 100644
index 0000000..c54294a
--- /dev/null
+++ b/app/Models/ProductPinYin.php
@@ -0,0 +1,26 @@
+belongsTo(User::class, 'user_id');
+ }
+}
diff --git a/app/Models/ScoreRule.php b/app/Models/ScoreRule.php
new file mode 100644
index 0000000..df230d1
--- /dev/null
+++ b/app/Models/ScoreRule.php
@@ -0,0 +1,84 @@
+addMinutes(5),
+ function () use ($code) {
+ return ScoreRule::query()
+ ->where('index_code', $code)
+ ->orderByDesc('times')
+ ->get();
+ }
+ );
+
+ /**
+ * @var $rule ScoreRule
+ */
+ foreach ($allRules as $rule) {
+ if ($times == $rule->times) {
+ return $rule;
+ }
+ }
+
+ return null;
+ }
+
+ public static function boot()
+ {
+ parent::bootTraits();
+
+ // 每当规则修改的时候, 移除掉缓存
+ self::saved(function () {
+
+ Cache::forget(self::CACHE_KEY);
+ });
+ self::deleted(function () {
+
+ Cache::forget(self::CACHE_KEY);
+ });
+ }
+}
diff --git a/app/Models/SearchAble/ElasticSearchTrait.php b/app/Models/SearchAble/ElasticSearchTrait.php
new file mode 100644
index 0000000..b7abcc4
--- /dev/null
+++ b/app/Models/SearchAble/ElasticSearchTrait.php
@@ -0,0 +1,258 @@
+setHosts($configs['hosts']);
+
+ self::$client = $builder->build();
+ }
+
+ return self::$client;
+ }
+
+ public static function info()
+ {
+ $instance = new static;
+
+
+ $index = array(
+ 'index' => $instance->getIndexName(),
+ 'client' => [
+ 'timeout' => 5, // ten second timeout
+ 'connect_timeout' => 5
+ ]
+ );
+
+ return self::client()->indices()->get($index);
+ }
+
+ public static function indexExists()
+ {
+ $instance = new static;
+
+
+ $index = array(
+ 'index' => $instance->getIndexName(),
+ 'client' => [
+ 'timeout' => 5, // ten second timeout
+ 'connect_timeout' => 5
+ ]
+ );
+
+ return self::client()->indices()->exists($index);
+ }
+
+
+ public static function deleteIndex()
+ {
+ $instance = new static;
+
+
+ $index = array(
+ 'index' => $instance->getIndexName(),
+ 'client' => [
+ 'timeout' => 5, // ten second timeout
+ 'connect_timeout' => 5
+ ]
+ );
+
+ return self::client()->indices()->delete($index);
+ }
+
+ public static function createIndex($shards = null, $replicas = null)
+ {
+ /**
+ * @var $instance ElasticSearchTrait
+ */
+ $instance = new static;
+
+ $client = self::client();
+
+ $index = array(
+ 'index' => $instance->getIndexName(),
+ );
+
+ $settings = $instance->getIndexSettings();
+ if (!is_null($settings)) {
+ $index['body']['settings'] = $settings;
+ }
+
+ if (!is_null($shards)) {
+ $index['body']['settings']['number_of_shards'] = $shards;
+ }
+
+ if (!is_null($replicas)) {
+ $index['body']['settings']['number_of_replicas'] = $replicas;
+ }
+
+ $mappingProperties = $instance->getMappingProperties();
+ if (!is_null($mappingProperties)) {
+ $index['body']['mappings'] = [
+ '_source' => array('enabled' => true),
+ 'properties' => $mappingProperties,
+ ];
+ }
+
+ return $client->indices()->create($index);
+ }
+
+ /**
+ * @return mixed
+ * @throws Exception
+ */
+ public function addToIndex($body = [])
+ {
+ if (!$this->exists) {
+ throw new Exception('Document does not exist.');
+ }
+
+ $params = $this->getBasicEsParams();
+
+ // Get our document body data.
+ $params['body'] = $body;
+
+ // The id for the document must always mirror the
+ // key for this model, even if it is set to something
+ // other than an auto-incrementing value. That way we
+ // can do things like remove the document from
+ // the index, or get the document from the index.
+ $params['id'] = $this->getKey();
+
+ return self::client()->index($params);
+ }
+
+ public function removeFromIndex()
+ {
+ return self::client()->delete($this->getBasicEsParams());
+ }
+
+ /**
+ * query: {
+ * "match": {
+ * "tweet": "elasticsearch"
+ * }
+ * }
+ * 排序:[
+ * { "post_date" : {"order" : "asc"}},
+ * "user",
+ * { "name" : "desc" },
+ * { "age" : "desc" },
+ * "_score"
+ * ]
+ * @param $query
+ * @param $limit
+ * @param $offset
+ * @param null $sort
+ * @param null $sourceFields
+ * @param null $aggregations
+ * @return array
+ */
+ public static function search($query, $limit = null, $offset = null, $sort = null, $sourceFields = null, $aggregations = null)
+ {
+ $instance = new static;
+
+ $params = $instance->getBasicEsParams(true, true, true, $limit, $offset);
+
+ if (!empty($sourceFields)) {
+ $params['body']['_source']['include'] = false;
+ }
+
+ if (!empty($query)) {
+ $params['body']['query'] = $query;
+ }
+
+ if (!empty($aggregations)) {
+ $params['body']['aggs'] = $aggregations;
+ }
+
+ if (!empty($sort)) {
+ $params['body']['sort'] = $sort;
+ }
+
+ $result = self::client()->search($params);
+ return $result;
+ }
+
+
+ public static function searchCount($query)
+ {
+ $instance = new static;
+
+ $params = $instance->getBasicEsParams(true, true, true, null, null);
+
+
+ if (!empty($query)) {
+ $params['body']['query'] = $query;
+ }
+
+
+ $result = self::client()->count($params);
+ return $result;
+ }
+
+ public function getBasicEsParams($getIdIfPossible = true, $getSourceIfPossible = false, $getTimestampIfPossible = false, $limit = null, $offset = null)
+ {
+ $params = array(
+ 'index' => $this->getIndexName(),
+ 'client' => [
+ 'timeout' => 5, // ten second timeout
+ 'connect_timeout' => 5
+ ]
+ );
+
+ if ($getIdIfPossible && $this->getKey()) {
+ $params['id'] = $this->getKey();
+ }
+
+
+ if (is_numeric($limit)) {
+ $params['size'] = $limit;
+ }
+
+ if (is_numeric($offset)) {
+ $params['from'] = $offset;
+ }
+
+ return $params;
+ }
+
+ public function getIndexName()
+ {
+ $indexName = config('elasticsearch.default_index', 'default');
+
+ return $indexName;
+ }
+
+ public function getIndexSettings()
+ {
+ return null;
+ }
+
+ public function getMappingProperties()
+ {
+ return [];
+ }
+
+}
diff --git a/app/Models/Seckill.php b/app/Models/Seckill.php
new file mode 100644
index 0000000..45aaabb
--- /dev/null
+++ b/app/Models/Seckill.php
@@ -0,0 +1,130 @@
+ 'double'
+ ];
+
+ protected $guarded = [];
+
+ /**
+ * 一个商品可以同时有多个商品同时秒杀,
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function product()
+ {
+ return $this->belongsTo(Product::class, 'product_id', 'id');
+ }
+
+
+ public static function boot()
+ {
+ parent::boot();
+
+
+ // 存入 redis
+ static::created(function (Seckill $seckill) {
+
+ // 从数据库中取出, 因为有一些默认值
+ $seckill = Seckill::query()->find($seckill->id);
+ $seckill->load('product');
+
+ // 以后的数据都将从 redis 取出,直至秒杀结束
+ Redis::set($seckill->getRedisModelKey(), $seckill->toJson());
+ // 填充一个 redis 队列,数量为抢购的数量,后面的 9 无意义
+ // 当去队列的值时,只需要判断是否为 null,就可以得知还有没有数量
+ $fill = array_fill(0, $seckill->number, 9);
+ Redis::lpush($seckill->getRedisQueueKey(), $fill);
+
+ // 秒杀商品,如果用户收藏,发送邮件提醒活动
+ RemindUsersHasSeckill::dispatch($seckill);
+ });
+ }
+
+
+ /**
+ * 获取所有的 redis key
+ *
+ * @return array
+ */
+ public function getAllRedisKey()
+ {
+ // 用户保存的 hset 需要匹配出来一个一个删
+ return [$this->getRedisModelKey(), $this->getRedisQueueKey()];
+ }
+
+ /**
+ * 存储抢到的用户
+ *
+ * @param $id
+ * @return string
+ */
+ public function getUsersKey($id)
+ {
+ return "seckills:{$this->id}:users:{$id}";
+ }
+
+
+ /**
+ * 存储模型 json 字符串
+ *
+ * @return string
+ */
+ public function getRedisModelKey()
+ {
+ return "seckills:{$this->id}:model";
+ }
+
+ /**
+ * 存储一个 秒杀数量的队列
+ *
+ * @return string
+ */
+ public function getRedisQueueKey()
+ {
+ return "seckills:{$this->id}:queue";
+ }
+}
diff --git a/app/Models/Setting.php b/app/Models/Setting.php
new file mode 100644
index 0000000..3685e75
--- /dev/null
+++ b/app/Models/Setting.php
@@ -0,0 +1,52 @@
+index_code), $setting->value);
+ });
+ }
+
+ public static function cacheKey($name)
+ {
+ return self::CACHE_KEY . $name;
+ }
+}
diff --git a/app/Models/SiteCount.php b/app/Models/SiteCount.php
new file mode 100644
index 0000000..b36ccb6
--- /dev/null
+++ b/app/Models/SiteCount.php
@@ -0,0 +1,44 @@
+belongsTo(User::class);
+ }
+}
diff --git a/app/Models/User.php b/app/Models/User.php
new file mode 100644
index 0000000..b5f20f4
--- /dev/null
+++ b/app/Models/User.php
@@ -0,0 +1,264 @@
+name)) {
+ return 'xxx';
+ }
+
+ $lastStr = mb_substr($this->name, 0, 1, 'utf-8');
+
+ $hiddenStr = str_repeat('*', mb_strlen($this->name, 'utf-8') - 1);
+
+ return $lastStr . $hiddenStr;
+ }
+
+
+ public function coupons()
+ {
+ return $this->hasMany(UserHasCoupon::class, 'user_id');
+ }
+
+ public function scoreLogs()
+ {
+ return $this->hasMany(ScoreLog::class, 'user_id');
+ }
+
+ public function addresses()
+ {
+ return $this->hasMany(Address::class);
+ }
+
+ public function subscribe()
+ {
+ return $this->hasOne(Subscribe::class);
+ }
+
+ public function cars()
+ {
+ return $this->hasMany(Car::class);
+ }
+
+ public function orders()
+ {
+ return $this->hasMany(Order::class);
+ }
+
+ /**
+ * 获取订单明细用以评论
+ */
+ public function orderDetails()
+ {
+ return $this->hasManyThrough(OrderDetail::class, Order::class);
+ }
+
+ public function products()
+ {
+ return $this->belongsToMany(Product::class, 'likes_products');
+ }
+
+
+ /**
+ * rewrite send reset password email
+ * @param string $token
+ */
+ public function sendPasswordResetNotification($token)
+ {
+ Mail::to($this->email)
+ ->queue(new ResetPassword($token));
+ }
+
+ /**
+ * 初始化头像
+ */
+ public static function boot()
+ {
+ parent::boot();
+
+ static::creating(function ($model) {
+
+ if (! isset($model->attributes['avatar'])) {
+ $model->attributes['avatar'] = 'avatars/default/' . array_random(User::DEFAULT_AVATARS);
+ }
+
+ if (! isset($model->attributes['password'])) {
+
+ $setting = new SettingKeyEnum(SettingKeyEnum::USER_INIT_PASSWORD);
+ $model->attributes['password'] = bcrypt(setting($setting, '123456'));
+ }
+
+ });
+
+ static::created(function ($model) {
+
+ // 用户注册之后,得到注册的来源
+ // 存入 redis 缓存,每日更新到统计表
+ $source = UserSourceEnum::search($model->source) ?: UserSourceEnum::search(UserSourceEnum::MOON);
+
+ $registerKey = SiteCountCacheEnum::MOON_REGISTER_COUNT;
+ switch ($source) {
+ case 'qq':
+ $registerKey = SiteCountCacheEnum::QQ_REGISTER_COUNT;
+ break;
+ case 'weibo':
+ $registerKey = SiteCountCacheEnum::WEIBO_REGISTER_COUNT;
+ break;
+ case 'github':
+ $registerKey = SiteCountCacheEnum::GITHUB_REGISTERED_COUNT;
+ break;
+ default:
+ break;
+ }
+
+ Cache::increment($registerKey);
+ Cache::increment(SiteCountCacheEnum::REGISTERED_COUNT);
+ });
+ }
+
+
+ public function getJWTCustomClaims()
+ {
+ return [];
+ }
+
+ public function getJWTIdentifier()
+ {
+ return $this->getKey();
+ }
+}
diff --git a/app/Models/UserHasCoupon.php b/app/Models/UserHasCoupon.php
new file mode 100644
index 0000000..d83398c
--- /dev/null
+++ b/app/Models/UserHasCoupon.php
@@ -0,0 +1,45 @@
+belongsTo(User::class, 'user_id');
+ }
+
+}
diff --git a/app/Notifications/ArticleTitleNotification.php b/app/Notifications/ArticleTitleNotification.php
new file mode 100644
index 0000000..c58cce8
--- /dev/null
+++ b/app/Notifications/ArticleTitleNotification.php
@@ -0,0 +1,58 @@
+article = $article;
+ }
+
+ /**
+ * Get the notification's delivery channels.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function via($notifiable)
+ {
+ return ['database'];
+ }
+
+ /**
+ * Get the mail representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return \Illuminate\Notifications\Messages\MailMessage
+ */
+ public function toMail($notifiable)
+ {
+ return (new MailMessage)
+ ->line('The introduction to the notification.')
+ ->action('Notification Action', url('/'))
+ ->line('Thank you for using our application!');
+ }
+
+ /**
+ * Get the array representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function toArray($notifiable)
+ {
+ return [
+ 'id' => $this->article->id,
+ 'title' => $this->article->title,
+ ];
+ }
+}
diff --git a/app/Notifications/CouponCodeNotification.php b/app/Notifications/CouponCodeNotification.php
new file mode 100644
index 0000000..f6d6a4d
--- /dev/null
+++ b/app/Notifications/CouponCodeNotification.php
@@ -0,0 +1,50 @@
+template = $template;
+ $this->code = $code;
+ }
+
+ /**
+ * Get the notification's delivery channels.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function via($notifiable)
+ {
+ return ['database'];
+ }
+
+
+ /**
+ * Get the array representation of the notification.
+ *
+ * @param mixed $notifiable
+ * @return array
+ */
+ public function toArray($notifiable)
+ {
+ return [
+ 'title' => $this->template->title . '兑换码',
+ 'start_date' => $this->template->start_date,
+ 'end_date' => $this->template->end_date,
+ 'code' => $this->code,
+ ];
+ }
+}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
new file mode 100644
index 0000000..013c809
--- /dev/null
+++ b/app/Providers/AppServiceProvider.php
@@ -0,0 +1,35 @@
+ 'App\Policies\ModelPolicy',
+ ];
+
+ /**
+ * Register any authentication / authorization services.
+ *
+ * @return void
+ */
+ public function boot()
+ {
+ $this->registerPolicies();
+ }
+}
diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php
new file mode 100644
index 0000000..352cce4
--- /dev/null
+++ b/app/Providers/BroadcastServiceProvider.php
@@ -0,0 +1,21 @@
+ [
+ 'App\Listeners\EventListener',
+ ],
+
+ ];
+
+ /**
+ * Register any events for your application.
+ *
+ * @return void
+ */
+ public function boot()
+ {
+ parent::boot();
+
+ //
+ }
+}
diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php
new file mode 100644
index 0000000..5e90b06
--- /dev/null
+++ b/app/Providers/RouteServiceProvider.php
@@ -0,0 +1,42 @@
+by($request->user()?->id ?: $request->ip());
+ });
+
+ $this->routes(function () {
+ Route::middleware('api')
+ ->prefix('api')
+ ->namespace('App\Http\Controllers')
+ ->group(base_path('routes/api.php'));
+
+ Route::middleware('web')
+ ->namespace('App\Http\Controllers')
+ ->group(base_path('routes/web.php'));
+ });
+ }
+}
diff --git a/app/Services/NotificationServe.php b/app/Services/NotificationServe.php
new file mode 100644
index 0000000..57eb9d7
--- /dev/null
+++ b/app/Services/NotificationServe.php
@@ -0,0 +1,59 @@
+type) {
+
+ case CouponCodeNotification::class:
+ $view = 'code';
+ break;
+ case ArticleTitleNotification::class:
+ $view = 'article';
+ break;
+ }
+ $view = "user.notifications.types.{$view}";
+
+ return $view;
+ }
+
+ public static function getTitle(DatabaseNotification $notification)
+ {
+ $data = $notification->data;
+
+ if (isset($data['title'])) {
+
+ return $data['title'];
+ }
+
+ return self::getContent($notification);
+ }
+
+ public static function getContent(DatabaseNotification $notification)
+ {
+ $title = '';
+ switch ($notification->type) {
+
+ case CouponCodeNotification::class:
+ $title = "你获得了新的优惠券兑换码,火速前往";
+ break;
+ case ArticleTitleNotification::class:
+ $title = "有新的文章发布了,火速查看";
+ break;
+ default:
+ $title = '默认消息';
+ break;
+ }
+
+ return $title;
+ }
+}
diff --git a/app/Services/OrderStatusButtonServe.php b/app/Services/OrderStatusButtonServe.php
new file mode 100644
index 0000000..470f00b
--- /dev/null
+++ b/app/Services/OrderStatusButtonServe.php
@@ -0,0 +1,107 @@
+order = $order;
+ }
+
+
+ public function cancelOrderButton()
+ {
+ $url = url("/user/orders/{$this->order->id}/cancel");
+
+ $this->buttons[] = <<