pull/1/head
li 8 months ago
parent 1e8d83658b
commit abbd10aada

@ -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

@ -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

11
.gitattributes vendored

@ -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

20
.gitignore vendored

@ -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

@ -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`
* 余额不足,请联系我及时充值
## 页面展示
![PC首页](public/media/pc_index.png)
![支付](public/media/pay.gif)
![个人设置](public/media/map_center.png)
![个人中心](public/media/center.png)
![积分详情](public/media/score_detail.png)
![后台仪表盘](public/media/admin/dash_board.png)
![后台订单列表](public/media/admin/orders.png)
![用户喜好数据](public/media/admin/user_like.png)
## 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
## 初始化抢购数据
<?php
// 假设当前秒杀活动的 id 为 9
// 可以在模型的 created 事件做这个事情
$id = 9;
// 填充一个 redis 队列,数量为抢购的数量,后面的 9 无意义
\Redis::lpush("seckills:{$id}:queue", array_fill(0, $seckill->number, 9));
?>
## 抢购
<?php
// 从路由或者参数中得到当前秒杀活动的 id
$id = 9;
$userId = auth()->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 (可选)
<?php
// 查出已经过期确没有回滚过的秒杀,
Seckill::query()
->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)
![](public/media/api_example.gif)
## 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)

@ -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.

@ -0,0 +1,22 @@
<?php
namespace App\Admin\Actions\Post;
use App\Models\User;
use Encore\Admin\Actions\RowAction;
use Illuminate\Database\Eloquent\Model;
class ActiveUserAction extends RowAction
{
public $name = '激活';
public function handle(User $model)
{
// $model ...
$model->is_active = 1;
$model->save();
return $this->response()->success('操作成功.')->refresh();
}
}

@ -0,0 +1,28 @@
<?php
namespace App\Admin\Actions\Post;
use Encore\Admin\Actions\RowAction;
use Illuminate\Database\Eloquent\Model;
class DividerAction extends RowAction
{
public $name = '分隔线';
public function handle(Model $model)
{
// $model ...
return $this->response()->success('Success message.')->refresh();
}
/**
* Render row action.
*
* @return string
*/
public function render()
{
return '<li class="divider"></li>';
}
}

@ -0,0 +1,30 @@
<?php
namespace App\Admin\Actions\Post;
use App\Models\Product;
use Encore\Admin\Actions\RowAction;
use Illuminate\Http\Request;
class ForceDeleteProductAction extends RowAction
{
public $name = '删除';
public function handle(Product $product)
{
$product->forceDelete();
return $this->response()->success('操作成功.')->refresh();
}
public function retrieveModel(Request $request)
{
if (!$key = $request->get('_key')) {
return false;
}
return Product::query()->withTrashed()->findOrFail($key);
}
}

@ -0,0 +1,34 @@
<?php
namespace App\Admin\Actions\Post;
use App\Enums\OrderShipStatusEnum;
use App\Enums\OrderStatusEnum;
use App\Models\Order;
use App\Models\User;
use Encore\Admin\Actions\RowAction;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
class OrderReceivedAction extends RowAction
{
public $name = '确认收货';
public function handle(Order $order)
{
if ($order->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();
}
}

@ -0,0 +1,59 @@
<?php
namespace App\Admin\Actions\Post;
use App\Enums\OrderShipStatusEnum;
use App\Enums\OrderStatusEnum;
use App\Models\Order;
use App\Models\User;
use Encore\Admin\Actions\RowAction;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Yansongda\Pay\Pay;
class OrderRefundAction extends RowAction
{
public $name = '退款';
public function handle(Order $order, Request $request)
{
// 订单必须申请支付了,才可才可以退款
if ($order->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('退款会直接把钱退回到支付账户,是否继续');
}
}

@ -0,0 +1,44 @@
<?php
namespace App\Admin\Actions\Post;
use App\Enums\OrderShipStatusEnum;
use App\Enums\OrderStatusEnum;
use App\Models\Order;
use App\Models\User;
use Encore\Admin\Actions\RowAction;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
class OrderShipAction extends RowAction
{
public $name = '发货';
public function handle(Order $order, Request $request)
{
if ($order->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();
}
}

@ -0,0 +1,46 @@
<?php
namespace App\Admin\Actions\Post;
use App\Models\Product;
use Encore\Admin\Actions\RowAction;
use Illuminate\Http\Request;
class ProductStatusAction extends RowAction
{
public $name = '';
public function setName($name)
{
$this->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);
}
}

@ -0,0 +1,118 @@
<?php
namespace App\Admin\Controllers;
use Encore\Admin\Auth\Database\Administrator;
use Encore\Admin\Auth\Database\OperationLog;
use Encore\Admin\Controllers\AuthController as BaseAuthController;
use Encore\Admin\Grid;
use Encore\Admin\Layout\Content;
use Encore\Admin\Widgets\Box;
use Encore\Admin\Widgets\InfoBox;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class AdminController extends BaseAuthController
{
// 覆盖默认的
public function index(Content $content)
{
return $content
->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 "<span class=\"badge bg-$color\">$method</span>";
});
$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) ?
'<code>{}</code>' :
'<pre>'.json_encode($input, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE).'</pre>';
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;
}
}

@ -0,0 +1,99 @@
<?php
namespace App\Admin\Controllers;
use App\Models\ArticleNotification;
use Encore\Admin\Controllers\AdminController;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Show;
class ArticleNotificationController extends AdminController
{
/**
* Title for current resource.
*
* @var string
*/
protected $title = '通知文章';
/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
$grid = new Grid(new ArticleNotification);
$grid->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;
}
}

@ -0,0 +1,87 @@
<?php
namespace App\Admin\Controllers;
use Encore\Admin\Auth\Database\Administrator;
use Encore\Admin\Controllers\AuthController as BaseAuthController;
use Encore\Admin\Facades\Admin;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Layout\Content;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class AuthController extends BaseAuthController
{
/**
* 覆盖默认的登录方法
*
* @param Request $request
*
* @return mixed
*/
public function postLogin(Request $request)
{
$credentials = $request->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);
}
}

@ -0,0 +1,90 @@
<?php
namespace App\Admin\Controllers;
use App\Models\Car;
use Encore\Admin\Controllers\AdminController;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Show;
class CarController extends AdminController
{
/**
* Title for current resource.
*
* @var string
*/
protected $title = '购物车数据';
/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
$grid = new Grid(new Car);
$grid->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;
}
}

@ -0,0 +1,197 @@
<?php
namespace App\Admin\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Category;
use Encore\Admin\Controllers\HasResourceActions;
use Encore\Admin\Form;
use Encore\Admin\Layout\Column;
use Encore\Admin\Layout\Content;
use Encore\Admin\Layout\Row;
use Encore\Admin\Show;
use Encore\Admin\Tree;
use Encore\Admin\Widgets\Box;
use Illuminate\Http\Request;
class CategoryController extends Controller
{
use HasResourceActions;
/**
* Index interface.
*
* @param Content $content
* @return Content
*/
public function index(Content $content)
{
return $content
->header('商品分类')
->description('')
->row(function (Row $row) {
// 只能在同一级排序拖动,不允许二级
$row->column(6, Category::tree(function (Tree $tree) {
$tree->disableCreate();
$tree->nestable(['maxDepth' => 1])
->branch(function ($branch) {
$icon = "<i class='fa {$branch['icon']}'></i>";
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']);
}
}

@ -0,0 +1,73 @@
<?php
namespace App\Admin\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Comment;
use App\Models\Product;
use App\Models\User;
use Encore\Admin\Controllers\HasResourceActions;
use Encore\Admin\Grid;
use Encore\Admin\Layout\Content;
class CommentController extends Controller
{
use HasResourceActions;
/**
* Index interface.
*
* @param Content $content
* @return Content
*/
public function index(Content $content)
{
return $content
->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;
}
}

@ -0,0 +1,190 @@
<?php
namespace App\Admin\Controllers;
use App\Enums\UserSexEnum;
use App\Enums\UserSourceEnum;
use App\Models\CouponCode;
use App\Models\CouponTemplate;
use App\Models\User;
use App\Notifications\CouponCodeNotification;
use Carbon\Carbon;
use Encore\Admin\Controllers\AdminController;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Show;
use Illuminate\Http\Request;
use Illuminate\Notifications\DatabaseNotification;
use Ramsey\Uuid\Uuid;
class CouponCodeController extends AdminController
{
/**
* Title for current resource.
*
* @var string
*/
protected $title = '优惠券兑换码';
/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
$grid = new Grid(new CouponCode);
$grid->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;
}
}

@ -0,0 +1,57 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Transforms\YesNoTransform;
use App\Models\UserHasCoupon;
use Encore\Admin\Controllers\AdminController;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Show;
class CouponLogController extends AdminController
{
/**
* Title for current resource.
*
* @var string
*/
protected $title = '会员领取记录';
/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
$grid = new Grid(new UserHasCoupon);
$grid->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;
}
}

@ -0,0 +1,86 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Transforms\YesNoTransform;
use App\Models\CouponTemplate;
use Carbon\Carbon;
use Encore\Admin\Controllers\AdminController;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Show;
class CouponTemplateController extends AdminController
{
/**
* Title for current resource.
*
* @var string
*/
protected $title = '优惠券';
/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
$grid = new Grid(new CouponTemplate);
$grid->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;
}
}

@ -0,0 +1,84 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Extensions\Div;
use App\Http\Controllers\Controller;
use App\Models\SiteCount;
use App\Models\User;
use App\Services\SiteCountService;
use Carbon\Carbon;
use Encore\Admin\Auth\Database\Menu;
use Encore\Admin\Facades\Admin;
use Encore\Admin\Layout\Content;
use Encore\Admin\Layout\Row;
use Encore\Admin\Widgets\Box;
class HomeController extends Controller
{
public function index(Content $content, SiteCountService $service)
{
// 用于更新菜单数据到文件, 可删除
// file_put_contents(database_path('data/menus.json'), Menu::all()->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');
}
}

@ -0,0 +1,192 @@
<?php
namespace App\Admin\Controllers;
use App\Models\Level;
use App\Http\Controllers\Controller;
use Encore\Admin\Controllers\HasResourceActions;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Layout\Content;
use Encore\Admin\Show;
class LevelController extends Controller
{
use HasResourceActions;
/**
* Index interface.
*
* @param Content $content
* @return Content
*/
public function index(Content $content)
{
return $content
->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);
}
}

@ -0,0 +1,267 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Actions\Post\OrderReceivedAction;
use App\Admin\Actions\Post\OrderRefundAction;
use App\Admin\Actions\Post\OrderShipAction;
use App\Admin\Extensions\ReceivedButton;
use App\Admin\Extensions\ShipButton;
use App\Admin\Transforms\OrderDetailTransform;
use App\Admin\Transforms\OrderPayTypeTransform;
use App\Admin\Transforms\OrderShipStatusTransform;
use App\Admin\Transforms\OrderStatusTransform;
use App\Admin\Transforms\OrderTypeTransform;
use App\Admin\Transforms\YesNoTransform;
use App\Enums\OrderShipStatusEnum;
use App\Enums\OrderStatusEnum;
use App\Http\Controllers\Controller;
use App\Models\Order;
use App\Models\User;
use Encore\Admin\Controllers\HasResourceActions;
use Encore\Admin\Facades\Admin;
use Encore\Admin\Grid;
use Encore\Admin\Grid\Displayers\Actions;
use Encore\Admin\Grid\Filter;
use Encore\Admin\Layout\Content;
use Encore\Admin\Show;
use Encore\Admin\Widgets\Form;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Yansongda\Pay\Pay;
class OrderController extends Controller
{
use HasResourceActions;
/**
* Index interface.
*
* @param Content $content
* @return Content
*/
public function index(Content $content)
{
return $content
->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);
}
}

@ -0,0 +1,249 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Actions\Post\DividerAction;
use App\Admin\Actions\Post\ForceDeleteProductAction;
use App\Admin\Actions\Post\ProductStatusAction;
use App\Admin\Transforms\YesNoTransform;
use App\Http\Controllers\Controller;
use App\Models\Category;
use App\Models\Product;
use Encore\Admin\Controllers\HasResourceActions;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Layout\Content;
use Encore\Admin\Show;
class ProductController extends Controller
{
use HasResourceActions;
/**
* Index interface.
*
* @param Content $content
* @return Content
*/
public function index(Content $content)
{
return $content
->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);
}
}

@ -0,0 +1,59 @@
<?php
namespace App\Admin\Controllers;
use App\Models\ProductHasUser;
use Encore\Admin\Controllers\AdminController;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Show;
class ProductLikeController extends AdminController
{
/**
* Title for current resource.
*
* @var string
*/
protected $title = '商品喜好';
/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
$grid = new Grid(new ProductHasUser);
$grid->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;
}
}

@ -0,0 +1,50 @@
<?php
namespace App\Admin\Controllers;
use App\Models\ScoreLog;
use Encore\Admin\Controllers\AdminController;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Show;
class ScoreLogController extends AdminController
{
/**
* Title for current resource.
*
* @var string
*/
protected $title = '积分日志';
/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
$grid = new Grid(new ScoreLog);
$grid->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;
}
}

@ -0,0 +1,227 @@
<?php
namespace App\Admin\Controllers;
use App\Enums\ScoreRuleIndexEnum;
use App\Models\ScoreRule;
use App\Http\Controllers\Controller;
use Encore\Admin\Controllers\HasResourceActions;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Layout\Content;
use Encore\Admin\Show;
use Illuminate\Http\Request;
class ScoreRuleController extends Controller
{
use HasResourceActions;
/**
* Index interface.
*
* @param Content $content
* @return Content
*/
public function index(Content $content)
{
return $content
->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);
}
}

@ -0,0 +1,222 @@
<?php
namespace App\Admin\Controllers;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreSeckillRequest;
use App\Models\Category;
use App\Models\Product;
use App\Models\Seckill;
use Carbon\Carbon;
use Encore\Admin\Controllers\HasResourceActions;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Layout\Content;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
class SeckillController extends Controller
{
use HasResourceActions;
/**
* Index interface.
*
* @param Content $content
* @return Content
*/
public function index(Content $content)
{
return $content
->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);
}
}

@ -0,0 +1,99 @@
<?php
namespace App\Admin\Controllers;
use App\Enums\SettingKeyEnum;
use App\Models\Setting;
use App\Http\Controllers\Controller;
use Encore\Admin\Controllers\HasResourceActions;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Layout\Content;
use Encore\Admin\Layout\Row;
use Encore\Admin\Show;
use Encore\Admin\Widgets\Box;
use Illuminate\Http\Request;
class SettingController extends Controller
{
use HasResourceActions;
/**
* Index interface.
*
* @param Content $content
* @return Content
*/
public function index(Content $content)
{
return $content
->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,
];
}
}

@ -0,0 +1,39 @@
<?php
namespace App\Admin\Controllers;
use App\Exceptions\UploadException;
use App\Http\Controllers\Controller;
use App\Services\UploadServe;
use Illuminate\Support\Facades\Storage;
class UploadController extends Controller
{
/**
* @param UploadServe $uploadServe
* @return array
*/
public function uploadByEditor(UploadServe $uploadServe)
{
$disk = 'public';
try {
$files = $uploadServe->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];
}
}

@ -0,0 +1,285 @@
<?php
namespace App\Admin\Controllers;
use App\Admin\Actions\Post\ActiveUserAction;
use App\Admin\Transforms\UserSexTransform;
use App\Admin\Transforms\YesNoTransform;
use App\Enums\UserSexEnum;
use App\Enums\UserStatusEnum;
use App\Http\Controllers\Controller;
use App\Models\Level;
use App\Models\User;
use Encore\Admin\Controllers\HasResourceActions;
use Encore\Admin\Form;
use Encore\Admin\Grid;
use Encore\Admin\Grid\Filter;
use Encore\Admin\Layout\Content;
use Encore\Admin\Show;
use Illuminate\Validation\Rule;
class UserController extends Controller
{
use HasResourceActions;
/**
* Index interface.
*
* @param Content $content
* @return Content
*/
public function index(Content $content)
{
return $content
->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;
}
}

@ -0,0 +1,44 @@
<?php
namespace App\Admin\Extensions;
use Illuminate\Contracts\Support\Renderable;
class Div implements Renderable
{
protected $id;
protected $width = '100%';
protected $height = '300px';
/**
* Div constructor.
*
* @param $id
* @param string $width
* @param string $height
*/
public function __construct($id, $width = null, $height = null)
{
$this->id = $id;
if (! is_null($width)) {
$this->width = $width;
}
if (! is_null($height)) {
$this->height = $height;
}
}
public function render()
{
return <<<div
<div id="{$this->id}" style="width: {$this->width}; height: {$this->height}"></div>
div;
}
}

@ -0,0 +1,56 @@
<?php
namespace App\Admin\Extensions;
use Illuminate\Support\Collection;
use Encore\Admin\Form\Field;
class WangEditor extends Field
{
protected $view = 'admin.wang-editor';
protected static $css = [
'/vendor/wangEditor-3.1.1/release/wangEditor.css',
];
protected static $js = [
'/vendor/wangEditor-3.1.1/release/wangEditor.js',
];
public function render()
{
$name = $this->formatName($this->column);
$token = csrf_token();
$url = admin_base_path('upload/editor');
$this->script = <<<EOT
var E = window.wangEditor
var editor = new E('#{$this->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();
}
}

@ -0,0 +1,26 @@
<?php
namespace App\Admin\Transforms;
use App\Enums\OrderPayTypeEnum;
use App\Enums\OrderShipStatusEnum;
use App\Enums\OrderStatusEnum;
use App\Enums\OrderTypeEnum;
use App\Models\Order;
class OrderPayTypeTransform implements Transform
{
public static function trans($type)
{
$text = '';
if ($type == OrderPayTypeEnum::ALI) {
$text = '支付宝';
} elseif ($type == OrderPayTypeEnum::WECHAT) {
$text = '微信';
}
return $text;
}
}

@ -0,0 +1,33 @@
<?php
namespace App\Admin\Transforms;
use App\Enums\OrderShipStatusEnum;
use App\Enums\OrderStatusEnum;
use App\Enums\OrderTypeEnum;
use App\Models\Order;
class OrderShipStatusTransform implements Transform
{
public static function trans($status)
{
switch ($status) {
case OrderShipStatusEnum::PENDING:
$text = '未发货';
break;
case OrderShipStatusEnum::DELIVERED:
$text = '待收货';
break;
case OrderShipStatusEnum::RECEIVED:
$text = '已收货';
break;
default:
$text = '未知状态';
break;
}
return $text;
}
}

@ -0,0 +1,45 @@
<?php
namespace App\Admin\Transforms;
use App\Enums\OrderShipStatusEnum;
use App\Enums\OrderStatusEnum;
use App\Enums\OrderTypeEnum;
use App\Models\Order;
class OrderStatusTransform implements Transform
{
public static function trans($status)
{
switch ($status) {
case OrderStatusEnum::UN_PAY_CANCEL:
$text = '取消';
break;
case OrderStatusEnum::REFUND:
$text = '退款';
break;
case OrderStatusEnum::APPLY_REFUND:
$text = '申请退款';
break;
case OrderStatusEnum::UN_PAY:
$text = '未支付';
break;
case OrderStatusEnum::PAID:
$text = '已支付';
break;
case OrderStatusEnum::TIMEOUT_CANCEL:
$text = '超时未付款系统自动取消';
break;
case OrderStatusEnum::COMPLETED:
$text = '完成';
break;
default:
$text = '未知状态';
break;
}
return $text;
}
}

@ -0,0 +1,25 @@
<?php
namespace App\Admin\Transforms;
use App\Enums\OrderShipStatusEnum;
use App\Enums\OrderStatusEnum;
use App\Enums\OrderTypeEnum;
use App\Models\Order;
class OrderTypeTransform implements Transform
{
public static function trans($type)
{
$text = '未知';
if ($type == OrderTypeEnum::COMMON) {
$text = '普通订单';
} elseif ($type == OrderTypeEnum::SEC_KILL) {
$text = '秒杀订单';
}
return $text;
}
}

@ -0,0 +1,9 @@
<?php
namespace App\Admin\Transforms;
interface Transform
{
public static function trans($trans);
}

@ -0,0 +1,24 @@
<?php
namespace App\Admin\Transforms;
use App\Enums\UserSexEnum;
use App\Enums\UserStatusEnum;
use App\Models\User;
class UserSexTransform implements Transform
{
public static function trans($sex)
{
$text = '未知';
if ($sex == UserSexEnum::MAN) {
$text = '男';
} elseif ($sex == UserSexEnum::WOMAN) {
$text = '女';
}
return $text;
}
}

@ -0,0 +1,16 @@
<?php
namespace App\Admin\Transforms;
use App\Enums\UserStatusEnum;
class YesNoTransform implements Transform
{
public static function trans($is)
{
return $is
? "<i style='color: green;' class=\"fa fa-check-circle\" aria-hidden=\"true\"></i>"
: "<i style='color: red;' class=\"fa fa-times\" aria-hidden=\"true\"></i>";
}
}

@ -0,0 +1,8 @@
<?php
use App\Admin\Extensions\WangEditor;
use Encore\Admin\Form;
Form::forget(['map', 'editor']);
Form::extend('editor', WangEditor::class);

@ -0,0 +1,76 @@
<?php
use App\Admin\Controllers\CouponLogController;
use Illuminate\Routing\Router;
Admin::registerAuthRoutes();
Route::group([
'prefix' => 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');
});

@ -0,0 +1,91 @@
<?php
namespace App\Console\Commands;
use App\Models\Product;
use App\Models\SearchAble\ElasticSearchTrait;
use Elasticsearch\ClientBuilder;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Collection;
class AddShopToEsSearchCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'add:shop-to-search';
/**
* The console command description.
*
* @var string
*/
protected $description = '把商品添加到全文索引';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
try {
ElasticSearchTrait::client()->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('索引生成完毕');
}
}

@ -0,0 +1,40 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class BaseCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:base';
/**
* The console command description.
*
* @var string
*/
protected $description = 'don`t use';
public function __construct()
{
parent::__construct();
}
public function execShellWithPrint($command)
{
$this->info('----------');
$this->info($command);
$output = shell_exec($command);
$this->info($output);
}
}

@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands;
class CacheOptimize extends BaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:cache';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Cache routing, configure information, to speed up the website running speed';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* 执行缓存命令
*
* @return mixed
*/
public function handle()
{
// 优化配置
$this->call('config:cache');
// 优化路由
$this->call('route:cache');
}
}

@ -0,0 +1,42 @@
<?php
namespace App\Console\Commands;
class ClearCache extends BaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:clear';
/**
* The console command description.
*
* @var string
*/
protected $description = 'clear moon:cache cache information';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* 清除所有缓存
*
* @return mixed
*/
public function handle()
{
$this->call('config:clear');
$this->call('route:clear');
$this->call('view:clear');
}
}

@ -0,0 +1,66 @@
<?php
namespace App\Console\Commands;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
class CopyFile extends BaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:copy';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Copy all upload static resources';
/**
* @var Filesystem
*/
protected $filesystem;
/**
* Create a new command instance.
*
* @param Filesystem $filesystem
*/
public function __construct(Filesystem $filesystem)
{
$this->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');
}
}

@ -0,0 +1,59 @@
<?php
namespace App\Console\Commands;
use App\Models\SiteCount;
use App\Services\SiteCountService;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class CountSite extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:count-site';
/**
* The console command description.
*
* @var string
*/
protected $description = '统计数据,直接执行会统计到昨天';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @param SiteCountService $service
* @return mixed
*/
public function handle(SiteCountService $service)
{
// 每天凌晨统计昨天的数据
$date = Carbon::now()->subDay(1)->toDateString();
/**
* 防止一天运行多次,所以采用增加
*
* @var $site SiteCount
*/
$site = SiteCount::query()->firstOrNew(compact('date'));
$site = $service->syncByCache($site, true);
$site->save();
createSystemLog('系统统计站点数据', $site->toArray());
}
}

@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use App\Services\ScoreLogServe;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class DelExpireScoreData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:del-score-data';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
* @throws \Psr\SimpleCache\InvalidArgumentException
*/
public function handle()
{
// 获取昨天的日期
$yesterday = Carbon::today()->subDay()->toDateString();
$serve = new ScoreLogServe();
Cache::delete($serve->loginKey($yesterday));
createSystemLog("系统删除{$yesterday}过期积分统计数据", ['date' => $yesterday]);
}
}

@ -0,0 +1,92 @@
<?php
namespace App\Console\Commands;
use App\Models\Seckill;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class DelExpireSecKill extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:del-seckills';
/**
* 删除过期的秒杀
*
* @var string
*/
protected $description = 'delete expire seckills';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$rollbacks = collect();
// 查出已经过期确没有回滚过的秒杀,
Seckill::query()
->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());
}
}
}

@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands;
use Illuminate\Support\Facades\Storage;
class DeleteFile extends BaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:delete';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete all upload static resources';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* 删除静态资源文件
*
* @return mixed
*/
public function handle()
{
Storage::deleteDirectory('public' . DIRECTORY_SEPARATOR . config('web.upload.list'));
Storage::deleteDirectory('public' . DIRECTORY_SEPARATOR . config('web.upload.detail'));
$this->info('delete file success');
}
}

@ -0,0 +1,56 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class ExportDatabaseCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:export';
/**
* The console command description.
*
* @var string
*/
protected $description = '导出数据到 json 文件';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$users = User::query()
->oldest()
->get()
->transform(function (User $user) {
// 先排除自身主键
$user->offsetUnset($user->getKeyName());
return $user;
});
file_put_contents(\UsersTableSeeder::DATA_PATH, $users->toJson(JSON_UNESCAPED_UNICODE));
}
}

@ -0,0 +1,71 @@
<?php
namespace App\Console\Commands;
use App\Models\Product;
class InstallShop extends BaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:install';
/**
* The console command description.
*
* @var string
*/
protected $description = '初始化商城项目';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* 安装商城
*
* @return mixed
*/
public function handle()
{
$this->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']);
}
}

@ -0,0 +1,55 @@
<?php
namespace App\Console\Commands;
use App\Mail\SubscribesNotice;
use App\Models\Subscribe;
use Illuminate\Console\Command;
use Mail;
class SendSubscribeEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:send-subscribes';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$mails = Subscribe::query()->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());
}
}

@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use App\Models\Product;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class SyncProducViewCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:sync-product_view';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$yesterday = Carbon::yesterday()->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]);
}
}

@ -0,0 +1,45 @@
<?php
namespace App\Console\Commands;
class UninstallShop extends BaseCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:uninstall';
/**
* The console command description.
*
* @var string
*/
protected $description = '卸载商城项目';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->call('moon:clear');
$this->call('migrate:reset');
// delete all upload static resources
$this->call('moon:delete');
}
}

@ -0,0 +1,55 @@
<?php
namespace App\Console\Commands;
use App\Enums\HomeCacheEnum;
use App\Models\Category;
use App\Models\Product;
use App\Models\User;
use App\Utils\HomeCacheDataUtil;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class UpdateCacheHomeData extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'moon:update-home';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// 每分钟有定时任务更新
$ttl = 60 * 2;
HomeCacheDataUtil::categories($ttl, true);
HomeCacheDataUtil::hotProducts($ttl, true);
HomeCacheDataUtil::latestProducts($ttl, true);
HomeCacheDataUtil::users($ttl, true);
}
}

@ -0,0 +1,60 @@
<?php
namespace App\Console;
use App\Console\Commands\CountSite;
use App\Console\Commands\DelExpireScoreData;
use App\Console\Commands\DelExpireSecKill;
use App\Console\Commands\SendSubscribeEmail;
use App\Console\Commands\SyncProducViewCommand;
use App\Console\Commands\UpdateCacheHomeData;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
//
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// 每周六八点发送订阅邮件
$schedule->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');
}
}

@ -0,0 +1,17 @@
<?php
namespace App\Enums;
use MyCLabs\Enum\Enum;
class HomeCacheEnum extends Enum
{
// CHANGE: 修改 kye 名,防止旧版本 key 永久缓存不修改
const SEC_KILL_DATA = 'home_page:sec_kill_data';
const CATEGORIES = 'home_page:categories';
const HOTTEST = 'home_page:hottest';
const LATEST = 'home_page:latest';
const USERS = 'home_page:users';
const COUPON_TEMPLATES = 'home_page:templates';
}

@ -0,0 +1,12 @@
<?php
namespace App\Enums;
use MyCLabs\Enum\Enum;
class OrderPayTypeEnum extends Enum
{
const ALI = 'ali';
const WECHAT = 'wechat';
}

@ -0,0 +1,17 @@
<?php
namespace App\Enums;
use MyCLabs\Enum\Enum;
class OrderShipStatusEnum extends Enum
{
// 未发货
const PENDING = 1;
// 待收货
const DELIVERED = 2;
// 已收货
const RECEIVED = 3;
}

@ -0,0 +1,26 @@
<?php
namespace App\Enums;
use MyCLabs\Enum\Enum;
class OrderStatusEnum extends Enum
{
const UN_PAY_CANCEL = 0;
// 未支付
const UN_PAY = 1;
// 已经支付
const PAID = 2;
// 订单完成
const COMPLETED = 3;
// 超时未支付
const TIMEOUT_CANCEL = 4;
// 申请退款
const APPLY_REFUND = 5;
// 退款
const REFUND = 6;
}

@ -0,0 +1,12 @@
<?php
namespace App\Enums;
use MyCLabs\Enum\Enum;
class OrderTypeEnum extends Enum
{
const COMMON = 1;
const SEC_KILL = 2;
}

@ -0,0 +1,15 @@
<?php
namespace App\Enums;
use MyCLabs\Enum\Enum;
class ScoreRuleIndexEnum extends Enum
{
const CONTINUE_LOGIN = 'continue_login';
const VISITED_PRODUCT = 'visited_product';
const COMPLETE_ORDER = 'complete_order';
const LOGIN = 'login';
const REGISTER = 'register';
}

@ -0,0 +1,14 @@
<?php
namespace App\Enums;
use MyCLabs\Enum\Enum;
class SettingKeyEnum extends Enum
{
const USER_INIT_PASSWORD = 'user_init_password';
const IS_OPEN_SECKILL = 'is_open_seckill';
const UN_PAY_CANCEL_TIME = 'order_un_pay_auto_cancel_time';
const POST_AMOUNT = 'post_amount';
}

@ -0,0 +1,25 @@
<?php
namespace App\Enums;
use MyCLabs\Enum\Enum;
class SiteCountCacheEnum extends Enum
{
// 注册的 key
const REGISTERED_COUNT = 'site_counts:registered_count';
const GITHUB_REGISTERED_COUNT = 'site_counts:github_registered_count';
const QQ_REGISTER_COUNT = 'site_counts:qq_registered_count';
const WEIBO_REGISTER_COUNT = 'site_counts:weibo_registered_count';
const MOON_REGISTER_COUNT = 'site_counts:moon_registered_count';
// 订单的 key 统计
const ORDER_COUNT = 'site_counts:order_count';
// 成功支付的订单
const PAY_ORDER_COUNT = 'site_counts:order_pay_count';
// 退款订单量
const REFUND_ORDER_COUNT = 'site_counts:refund_pay_count';
// 销售金额
const SALE_ORDER_COUNT = 'site_counts:sale_money_count';
}

@ -0,0 +1,12 @@
<?php
namespace App\Enums;
use MyCLabs\Enum\Enum;
class UserSexEnum extends Enum
{
const MAN = 1;
const WOMAN = 2;
}

@ -0,0 +1,15 @@
<?php
namespace App\Enums;
use MyCLabs\Enum\Enum;
class UserSourceEnum extends Enum
{
// 默认上城创建
const MOON = 1;
const GITHUB = 2;
const QQ = 3;
const WEIBO = 4;
}

@ -0,0 +1,12 @@
<?php
namespace App\Enums;
use MyCLabs\Enum\Enum;
class UserStatusEnum extends Enum
{
const ACTIVE = 1;
const UN_ACTIVE = 0;
}

@ -0,0 +1,36 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class Event
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class BadRequestException extends Exception
{
//
}

@ -0,0 +1,62 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
use Tymon\JWTAuth\Exceptions\JWTException;
use Tymon\JWTAuth\Exceptions\TokenBlacklistedException;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
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);
});
}
}

@ -0,0 +1,15 @@
<?php
/**
* Created by PhpStorm.
* User: David
* Date: 2018/10/19
* Time: 22:04
*/
namespace App\Exceptions;
class OrderException extends \Exception
{
}

@ -0,0 +1,15 @@
<?php
/**
* Created by PhpStorm.
* User: David
* Date: 2018/10/19
* Time: 22:04
*/
namespace App\Exceptions;
class UploadException extends \Exception
{
}

@ -0,0 +1,104 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Enums\UserSexEnum;
use App\Enums\UserStatusEnum;
use App\Exceptions\BadRequestException;
use App\Http\Controllers\Controller;
use App\Http\Requests\LoginRequest;
use App\Http\Requests\RegisterUserRequest;
use App\Http\Resources\OwnResource;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
/**
* 登录的接口
*
* @param LoginRequest $request
* @return \Illuminate\Http\JsonResponse
*/
public function login(LoginRequest $request)
{
$username = $request->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');
}
}

@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Resources\CategoreResource;
use App\Http\Resources\ProductResource;
use App\Models\Category;
use App\Services\PageServe;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class CategoryController extends Controller
{
public function index(PageServe $serve)
{
list($limit, $offset) = $serve->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'));
}
}

@ -0,0 +1,14 @@
<?php
namespace App\Http\Controllers\Api\V1;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class HomeController extends Controller
{
public function ping()
{
return responseJsonAsUnAuthorized();
}
}

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Resources\OwnResource;
use App\Http\Resources\ScoreLogResource;
use App\Models\User;
use App\Services\PageServe;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class OwnController extends Controller
{
public function me()
{
$me = auth()->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'));
}
}

@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Cache;
class ProductController extends Controller
{
public function show($uuid)
{
$product = Product::query()->where('uuid', $uuid)->firstOrFail();
$product->load('detail');
// 直接使用缓存
$today = Carbon::today()->toDateString();
Cache::increment($product->getViewCountKey($today));
return responseJson(200, 'success', new ProductResource($product));
}
}

@ -0,0 +1,161 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Enums\UserSourceEnum;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Overtrue\Socialite\AuthorizeFailedException;
use Overtrue\Socialite\SocialiteManager;
class AuthLoginController extends Controller
{
protected $allow = ['github', 'qq', 'weibo'];
/**
* 第三方授权登录跳转
*
* @param $driver
* @return mixed
*/
public function redirectToAuth($driver)
{
if (! in_array($driver, $this->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', '解绑成功');
}
}

@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Support\Facades\Auth;
class ForgotPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
use SendsPasswordResetEmails;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
protected function guard()
{
return auth()->guard();
}
}

@ -0,0 +1,167 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Enums\UserStatusEnum;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\ScoreLogServe;
use App\Services\UserService;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Foundation\Auth\RedirectsUsers;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\URL;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
use RedirectsUsers, ThrottlesLogins;
protected $redirectTo = '/';
// 排除记录的回跳 url, 防止重复跳转
protected $except = ['*login*', '*register*'];
/**
* LoginController constructor.
*/
public function __construct()
{
$this->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';
}
}

@ -0,0 +1,145 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Enums\UserSexEnum;
use App\Http\Controllers\Controller;
use App\Mail\UserRegister;
use App\Models\User;
use Gregwar\Captcha\CaptchaBuilder;
use Illuminate\Auth\Events\Registered;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use RegistersUsers;
/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->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();
}
}

@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Where to redirect users after resetting their password.
*
* @var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->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();
}
}

@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Mail\UserRegister;
use App\Models\User;
use Illuminate\Support\Facades\Mail;
class UserController extends Controller
{
public function activeAccount($token)
{
if ($user = User::query()->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' => '用户名或者密码错误']);
}
}

@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers;
use App\Models\Car;
use App\Models\Product;
use App\Models\User;
use Illuminate\Http\Request;
class CarController extends Controller
{
public function __construct()
{
$this->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, '删除成功');
}
}

@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Category;
use Illuminate\Http\Request;
class CategoryController extends Controller
{
/**
* 前台分类列表
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index()
{
$categories = Category::query()->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'));
}
}

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers;
use App\Models\CouponTemplate;
use App\Models\ScoreLog;
use App\Models\User;
use App\Models\UserHasCoupon;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class CouponController extends Controller
{
public function store(Request $request)
{
$templateId = $request->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, '领取成功');
}
}

@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers;
use App\Models\CouponTemplate;
use Carbon\Carbon;
use Illuminate\Http\Request;
class CouponTemplateController extends Controller
{
public function index()
{
$today = Carbon::today()->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'));
}
}

@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers;
use App\Enums\SettingKeyEnum;
use App\Models\Subscribe;
use App\Models\User;
use App\Utils\HomeCacheDataUtil;
class HomeController extends Controller
{
/**
* 首页显示的数据
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index()
{
// 取出后台排序好的九个分类,并且关联出商品的总数
// 如没有 key存入缓存中防止用户未配置好任务调度确访问首页
// 数据将不会从首页更新,每分钟任务调度更新,请务必配置好
$ttl = 120 + mt_rand(10, 30);
$categories = HomeCacheDataUtil::categories($ttl);
$hotProducts = HomeCacheDataUtil::hotProducts($ttl);
$latestProducts = HomeCacheDataUtil::latestProducts($ttl);
$users = HomeCacheDataUtil::users($ttl);
// 秒杀数据
$secKills = HomeCacheDataUtil::getSeckillData();
/**
* 当前登录用户
*
* @var $loginUser User
*/
if ($loginUser = auth()->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' => '已取消订阅']);
}
}

@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers;
use App\Admin\Transforms\OrderPayTypeTransform;
use App\Enums\OrderPayTypeEnum;
use App\Enums\OrderStatusEnum;
use App\Http\Controllers\User\PaymentController;
use App\Models\Order;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Yansongda\Pay\Pay;
class PaymentNotificationController extends Controller
{
protected $config;
public function __construct()
{
$this->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'));
}
}

@ -0,0 +1,147 @@
<?php
namespace App\Http\Controllers;
use App\Models\Node;
use App\Models\Product;
use App\Models\ProductPinYin;
use App\Models\User;
use App\Services\ScoreLogServe;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
class ProductController extends Controller
{
/**
* 商品列表
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index()
{
// 随机查出一些商品展示
$products = Product::query()->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();
}
}

@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Requests\AddressRequest;
use App\Models\Address;
use App\Models\User;
use Auth;
use DB;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class AddressController extends Controller
{
public function index()
{
$addresses = auth()->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();
}
}

@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers\User;
use App\Models\Product;
use App\Models\User;
use Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class LikesController extends Controller
{
protected $response = [
'code' => 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' => '收藏成功'
]);
}
}

@ -0,0 +1,149 @@
<?php
namespace App\Http\Controllers\User;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Notifications\CouponCodeNotification;
use App\Services\NotificationServe;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Notifications\DatabaseNotification;
class NotificationController extends Controller
{
public function index(Request $request)
{
/**
* @var $user User
*/
$user = auth()->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'));
}
}

@ -0,0 +1,326 @@
<?php
namespace App\Http\Controllers\User;
use App\Admin\Transforms\OrderShipStatusTransform;
use App\Admin\Transforms\OrderStatusTransform;
use App\Enums\OrderShipStatusEnum;
use App\Enums\OrderStatusEnum;
use App\Enums\ScoreRuleIndexEnum;
use App\Http\Controllers\Controller;
use App\Models\Address;
use App\Models\Comment;
use App\Models\Order;
use App\Models\OrderDetail;
use App\Models\ScoreRule;
use App\Models\User;
use App\Services\OrderStatusButtonServe;
use App\Services\ScoreLogServe;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Yansongda\Pay\Pay;
class OrderController extends Controller
{
public function index()
{
// 获取积分比例
$scoreRatio = $this->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', '删除成功');
}
}

@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\User;
use App\Enums\SettingKeyEnum;
use App\Exceptions\OrderException;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreOrderRequest;
use App\Jobs\CancelUnPayOrder;
use App\Models\Address;
use App\Models\Car;
use App\Models\Order;
use App\Models\Product;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Jenssegers\Agent\Agent;
use Yansongda\Pay\Pay;
class PaymentController extends Controller
{
/**
* 再次支付的接口
*
* @param $id
* @return \Symfony\Component\HttpFoundation\Response
*/
public function againStore($id)
{
/**
* @var $masterOrder Order
*/
$masterOrder = Order::query()->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);
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save