Browse Source

feat: 纯瘾大 ThinkPHP 8 后端 v2.1 R3修复版

- 5 Controller: Card/Menu/Order/Message/Staff (16 API)
- 5 Model: Product/Order/OrderItem/Message/Staff
- 2 Service: CardService(号码生成+冲突检测)/OrderService(下单事务+催单)
- 1 Middleware: StaffAuth(HMAC-SHA256无状态Token)
- BUG-01修复: 催单归属+30s冷却+5次上限
- BUG-02修复: 订单状态机校验(confirm/done/cancel)
- BUG-03修复: 消息输入校验(长度/枚举/XSS)
- BUG-04修复: 下单qty 1-99校验
- APP_DEBUG=false 生产就绪
dev
mac 1 day ago
commit
2f7039526a
55 changed files with 1571 additions and 0 deletions
  1. +11
    -0
      .env.example
  2. +11
    -0
      .example.env
  3. +6
    -0
      .gitignore
  4. +42
    -0
      .travis.yml
  5. +32
    -0
      LICENSE.txt
  6. +77
    -0
      README.md
  7. +1
    -0
      app/.htaccess
  8. +22
    -0
      app/AppService.php
  9. +94
    -0
      app/BaseController.php
  10. +58
    -0
      app/ExceptionHandle.php
  11. +8
    -0
      app/Request.php
  12. +2
    -0
      app/common.php
  13. +35
    -0
      app/controller/Card.php
  14. +18
    -0
      app/controller/Index.php
  15. +52
    -0
      app/controller/Menu.php
  16. +76
    -0
      app/controller/Message.php
  17. +58
    -0
      app/controller/Order.php
  18. +180
    -0
      app/controller/Staff.php
  19. +17
    -0
      app/event.php
  20. +10
    -0
      app/middleware.php
  21. +47
    -0
      app/middleware/StaffAuth.php
  22. +13
    -0
      app/model/Message.php
  23. +15
    -0
      app/model/Order.php
  24. +15
    -0
      app/model/OrderItem.php
  25. +20
    -0
      app/model/Product.php
  26. +10
    -0
      app/model/Staff.php
  27. +9
    -0
      app/provider.php
  28. +9
    -0
      app/service.php
  29. +41
    -0
      app/service/CardService.php
  30. +122
    -0
      app/service/OrderService.php
  31. +49
    -0
      composer.json
  32. +14
    -0
      config/app.php
  33. +29
    -0
      config/cache.php
  34. +9
    -0
      config/console.php
  35. +20
    -0
      config/cookie.php
  36. +31
    -0
      config/database.php
  37. +24
    -0
      config/filesystem.php
  38. +29
    -0
      config/lang.php
  39. +45
    -0
      config/log.php
  40. +7
    -0
      config/middleware.php
  41. +55
    -0
      config/route.php
  42. +19
    -0
      config/session.php
  43. +10
    -0
      config/trace.php
  44. +25
    -0
      config/view.php
  45. +2
    -0
      extend/.gitignore
  46. +8
    -0
      public/.htaccess
  47. +0
    -0
      public/drink
  48. BIN
      public/favicon.ico
  49. +25
    -0
      public/index.php
  50. +2
    -0
      public/robots.txt
  51. +19
    -0
      public/router.php
  52. +2
    -0
      public/static/.gitignore
  53. +24
    -0
      route/app.php
  54. +11
    -0
      think
  55. +1
    -0
      view/README.md

+ 11
- 0
.env.example View File

@ -0,0 +1,11 @@
APP_DEBUG = false
DB_TYPE = mysql
DB_HOST = 127.0.0.1
DB_NAME = drink
DB_USER = drink_app
DB_PASS = your_password_here
DB_PORT = 3306
DB_CHARSET = utf8mb4
DEFAULT_LANG = zh-cn

+ 11
- 0
.example.env View File

@ -0,0 +1,11 @@
APP_DEBUG = true
DB_TYPE = mysql
DB_HOST = 127.0.0.1
DB_NAME = test
DB_USER = username
DB_PASS = password
DB_PORT = 3306
DB_CHARSET = utf8
DEFAULT_LANG = zh-cn

+ 6
- 0
.gitignore View File

@ -0,0 +1,6 @@
vendor/
runtime/
.env
.DS_Store
*.log
composer.lock

+ 42
- 0
.travis.yml View File

@ -0,0 +1,42 @@
sudo: false
language: php
branches:
only:
- stable
cache:
directories:
- $HOME/.composer/cache
before_install:
- composer self-update
install:
- composer install --no-dev --no-interaction --ignore-platform-reqs
- zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip .
- composer require --update-no-dev --no-interaction "topthink/think-image:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0"
- composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0"
- composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0"
- zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip .
script:
- php think unit
deploy:
provider: releases
api_key:
secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw=
file:
- ThinkPHP_Core.zip
- ThinkPHP_Full.zip
skip_cleanup: true
on:
tags: true

+ 32
- 0
LICENSE.txt View File

@ -0,0 +1,32 @@
ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
版权所有Copyright © 2006-2025 by ThinkPHP (http://thinkphp.cn)
All rights reserved。
ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
Apache Licence是著名的非盈利开源组织Apache采用的协议。
该协议和BSD类似,鼓励代码共享和尊重原作者的著作权,
允许代码修改,再作为开源或商业软件发布。需要满足
的条件:
1. 需要给代码的用户一份Apache Licence ;
2. 如果你修改了代码,需要在被修改的文件中说明;
3. 在延伸的代码中(修改和有源代码衍生的代码中)需要
带有原来代码中的协议,商标,专利声明和其他原来作者规
定需要包含的说明;
4. 如果再发布的产品中包含一个Notice文件,则在Notice文
件中需要带有本协议内容。你可以在Notice中增加自己的
许可,但不可以表现为对Apache Licence构成更改。
具体的协议参考:http://www.apache.org/licenses/LICENSE-2.0
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

+ 77
- 0
README.md View File

@ -0,0 +1,77 @@
![](https://www.thinkphp.cn/uploads/images/20230630/300c856765af4d8ae758c503185f8739.png)
ThinkPHP 8
===============
## 特性
* 基于PHP`8.0+`重构
* 升级`PSR`依赖
* 依赖`think-orm`3.0+版本
* 全新的`think-dumper`服务,支持远程调试
* 支持`6.0`/`6.1`无缝升级
> ThinkPHP8的运行环境要求PHP8.0+
现在开始,你可以使用官方提供的[ThinkChat](https://chat.topthink.com/),让你在学习ThinkPHP的旅途中享受私人AI助理服务!
![](https://www.topthink.com/uploads/assistant/20230630/4d1a3f0ad2958b49bb8189b7ef824cb0.png)
ThinkPHP生态服务由[顶想云](https://www.topthink.com)(TOPThink Cloud)提供,为生态提供专业的开发者服务和价值之选。
## 文档
[完全开发手册](https://doc.thinkphp.cn)
## 赞助
全新的[赞助计划](https://www.thinkphp.cn/sponsor)可以让你通过我们的网站、手册、欢迎页及GIT仓库获得巨大曝光,同时提升企业的品牌声誉,也更好保障ThinkPHP的可持续发展。
[![](https://www.thinkphp.cn/sponsor/special.svg)](https://www.thinkphp.cn/sponsor/special)
[![](https://www.thinkphp.cn/sponsor.svg)](https://www.thinkphp.cn/sponsor)
## 安装
~~~
composer create-project topthink/think tp
~~~
启动服务
~~~
cd tp
php think run
~~~
然后就可以在浏览器中访问
~~~
http://localhost:8000
~~~
如果需要更新框架使用
~~~
composer update topthink/framework
~~~
## 命名规范
`ThinkPHP`遵循PSR-2命名规范和PSR-4自动加载规范。
## 参与开发
直接提交PR或者Issue即可
## 版权信息
ThinkPHP遵循Apache2开源协议发布,并提供免费使用。
本项目包含的第三方源码和二进制文件之版权信息另行标注。
版权所有Copyright © 2006-2024 by ThinkPHP (http://thinkphp.cn) All rights reserved。
ThinkPHP® 商标和著作权所有者为上海顶想信息科技有限公司。
更多细节参阅 [LICENSE.txt](LICENSE.txt)

+ 1
- 0
app/.htaccess View File

@ -0,0 +1 @@
deny from all

+ 22
- 0
app/AppService.php View File

@ -0,0 +1,22 @@
<?php
declare (strict_types = 1);
namespace app;
use think\Service;
/**
* 应用服务类
*/
class AppService extends Service
{
public function register()
{
// 服务注册
}
public function boot()
{
// 服务启动
}
}

+ 94
- 0
app/BaseController.php View File

@ -0,0 +1,94 @@
<?php
declare (strict_types = 1);
namespace app;
use think\App;
use think\exception\ValidateException;
use think\Validate;
/**
* 控制器基础类
*/
abstract class BaseController
{
/**
* Request实例
* @var \think\Request
*/
protected $request;
/**
* 应用实例
* @var \think\App
*/
protected $app;
/**
* 是否批量验证
* @var bool
*/
protected $batchValidate = false;
/**
* 控制器中间件
* @var array
*/
protected $middleware = [];
/**
* 构造方法
* @access public
* @param App $app 应用对象
*/
public function __construct(App $app)
{
$this->app = $app;
$this->request = $this->app->request;
// 控制器初始化
$this->initialize();
}
// 初始化
protected function initialize()
{}
/**
* 验证数据
* @access protected
* @param array $data 数据
* @param string|array $validate 验证器名或者验证规则数组
* @param array $message 提示信息
* @param bool $batch 是否批量验证
* @return array|string|true
* @throws ValidateException
*/
protected function validate(array $data, string|array $validate, array $message = [], bool $batch = false)
{
if (is_array($validate)) {
$v = new Validate();
$v->rule($validate);
} else {
if (strpos($validate, '.')) {
// 支持场景
[$validate, $scene] = explode('.', $validate);
}
$class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
$v = new $class();
if (!empty($scene)) {
$v->scene($scene);
}
}
$v->message($message);
// 是否批量验证
if ($batch || $this->batchValidate) {
$v->batch(true);
}
return $v->failException(true)->check($data);
}
}

+ 58
- 0
app/ExceptionHandle.php View File

@ -0,0 +1,58 @@
<?php
namespace app;
use think\db\exception\DataNotFoundException;
use think\db\exception\ModelNotFoundException;
use think\exception\Handle;
use think\exception\HttpException;
use think\exception\HttpResponseException;
use think\exception\ValidateException;
use think\Response;
use Throwable;
/**
* 应用异常处理类
*/
class ExceptionHandle extends Handle
{
/**
* 不需要记录信息(日志)的异常类列表
* @var array
*/
protected $ignoreReport = [
HttpException::class,
HttpResponseException::class,
ModelNotFoundException::class,
DataNotFoundException::class,
ValidateException::class,
];
/**
* 记录异常信息(包括日志或者其它方式记录)
*
* @access public
* @param Throwable $exception
* @return void
*/
public function report(Throwable $exception): void
{
// 使用内置的方式记录异常日志
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @access public
* @param \think\Request $request
* @param Throwable $e
* @return Response
*/
public function render($request, Throwable $e): Response
{
// 添加自定义异常处理机制
// 其他错误交给系统处理
return parent::render($request, $e);
}
}

+ 8
- 0
app/Request.php View File

@ -0,0 +1,8 @@
<?php
namespace app;
// 应用请求对象类
class Request extends \think\Request
{
}

+ 2
- 0
app/common.php View File

@ -0,0 +1,2 @@
<?php
// 应用公共文件

+ 35
- 0
app/controller/Card.php View File

@ -0,0 +1,35 @@
<?php
namespace app\controller;
use app\BaseController;
use app\service\CardService;
use app\service\OrderService;
class Card extends BaseController
{
// POST api/card/generate — 生成新号码牌
public function generate(CardService $cardService)
{
$cardNo = $cardService->generate();
return json(['code' => 0, 'data' => ['cardNo' => $cardNo], 'msg' => 'ok']);
}
// GET api/card/check?no=K182 — 校验号码牌
public function check(CardService $cardService, OrderService $orderService)
{
$no = $this->request->get('no', '');
if (!preg_match('/^[A-Z]\d{3}$/', $no)) {
return json(['code' => -1, 'data' => null, 'msg' => '号码牌格式错误']);
}
$orders = $orderService->listByCard($no);
return json([
'code' => 0,
'data' => [
'valid' => true,
'orders' => $orders,
],
'msg' => 'ok',
]);
}
}

+ 18
- 0
app/controller/Index.php View File

@ -0,0 +1,18 @@
<?php
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index()
{
return '<style>*{ padding: 0; margin: 0; }</style><iframe src="https://www.thinkphp.cn/welcome?version=' . \think\facade\App::version() . '" width="100%" height="100%" frameborder="0" scrolling="auto"></iframe>';
}
public function hello($name = 'ThinkPHP8')
{
return 'hello,' . $name;
}
}

+ 52
- 0
app/controller/Menu.php View File

@ -0,0 +1,52 @@
<?php
namespace app\controller;
use app\BaseController;
use app\model\Product;
class Menu extends BaseController
{
// GET api/menu/categories — 获取分类列表
public function categories()
{
$categories = Product::where('status', 1)
->field('category')
->group('category')
->order('sort_order', 'asc')
->column('category');
return json(['code' => 0, 'data' => $categories, 'msg' => 'ok']);
}
// GET api/menu/products?cate=经典鸡尾酒 — 按分类获取商品(不含recipe)
public function products()
{
$cate = $this->request->get('cate', '');
$query = Product::where('status', 1)->order('sort_order', 'asc');
if (!empty($cate) && $cate !== 'all') {
$query->where('category', $cate);
}
$products = $query->field('id,category,name,en,emoji,image_url,price,original_price,alc,desc')
->select()
->toArray();
return json(['code' => 0, 'data' => $products, 'msg' => 'ok']);
}
// GET api/menu/product?id=5 — 商品详情(不含recipe)
public function detail()
{
$id = $this->request->get('id', 0);
$product = Product::where('status', 1)
->field('id,category,name,en,emoji,image_url,price,original_price,alc,desc')
->find($id);
if (!$product) {
return json(['code' => -1, 'data' => null, 'msg' => '商品不存在']);
}
return json(['code' => 0, 'data' => $product->toArray(), 'msg' => 'ok']);
}
}

+ 76
- 0
app/controller/Message.php View File

@ -0,0 +1,76 @@
<?php
namespace app\controller;
use app\BaseController;
use app\model\Message as MessageModel;
class Message extends BaseController
{
// BUG-03: 聊天记录读取保持原有逻辑
public function list()
{
$cardNo = $this->request->get('card_no', '');
$since = $this->request->get('since', 0);
if (empty($cardNo)) {
return json(['code' => -1, 'data' => null, 'msg' => '缺少号码牌']);
}
$query = MessageModel::where('card_no', $cardNo)
->order('created_at', 'asc');
if ($since > 0) {
$query->where('id', '>', intval($since));
}
$messages = $query->select()->toArray();
$list = array_map(function ($m) {
return [
'id' => $m['id'],
'cardNo' => $m['card_no'],
'senderType' => $m['sender_type'],
'content' => $m['content'],
'time' => date('H:i', strtotime($m['created_at'])),
'staffId' => $m['staff_id'] ?? null,
];
}, $messages);
return json(['code' => 0, 'data' => $list, 'msg' => 'ok']);
}
// BUG-03: 增加输入校验 + XSS防护
public function send()
{
$cardNo = $this->request->post('cardNo', '');
$senderType = $this->request->post('senderType', 'customer');
$content = $this->request->post('content', '');
$staffId = $this->request->post('staffId', null);
// 必填校验
if (empty($cardNo) || empty($content)) {
return json(['code' => -1, 'data' => null, 'msg' => '参数不完整']);
}
// senderType 枚举校验
if (!in_array($senderType, ['customer', 'staff', 'system'])) {
return json(['code' => -1, 'data' => null, 'msg' => '发送者类型无效']);
}
// 内容长度校验 (DB VARCHAR 500)
if (mb_strlen($content) > 500) {
return json(['code' => -1, 'data' => null, 'msg' => '消息过长,最多500字']);
}
// XSS 防护 — HTML 转义
$content = htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
$msg = MessageModel::create([
'card_no' => $cardNo,
'sender_type' => $senderType,
'staff_id' => $staffId,
'content' => $content,
]);
return json(['code' => 0, 'data' => ['id' => $msg->id], 'msg' => 'ok']);
}
}

+ 58
- 0
app/controller/Order.php View File

@ -0,0 +1,58 @@
<?php
namespace app\controller;
use app\BaseController;
use app\service\OrderService;
class Order extends BaseController
{
public function submit(OrderService $orderService)
{
$cardNo = $this->request->post('cardNo', '');
$items = $this->request->post('items', []);
$note = $this->request->post('note', '');
if (empty($cardNo) || !preg_match('/^[A-Z]\d{3}$/', $cardNo)) {
return json(['code' => -1, 'data' => null, 'msg' => '号码牌无效']);
}
if (empty($items)) {
return json(['code' => -1, 'data' => null, 'msg' => '购物车为空']);
}
try {
$result = $orderService->submit($cardNo, $items, $note);
return json(['code' => 0, 'data' => $result, 'msg' => '下单成功']);
} catch (\Exception $e) {
return json(['code' => -1, 'data' => null, 'msg' => $e->getMessage()]);
}
}
public function list(OrderService $orderService)
{
$cardNo = $this->request->get('card_no', '');
if (empty($cardNo)) {
return json(['code' => -1, 'data' => null, 'msg' => '缺少号码牌']);
}
$orders = $orderService->listByCard($cardNo);
return json(['code' => 0, 'data' => $orders, 'msg' => 'ok']);
}
// BUG-01: 增加 cardNo 归属校验
public function remind(OrderService $orderService)
{
$id = $this->request->param('id', 0);
$cardNo = $this->request->param('card_no', '');
if (empty($id) || empty($cardNo)) {
return json(['code' => -1, 'data' => null, 'msg' => '参数不完整']);
}
try {
$orderService->remind(intval($id), $cardNo);
return json(['code' => 0, 'data' => null, 'msg' => '已催单']);
} catch (\Exception $e) {
return json(['code' => -1, 'data' => null, 'msg' => $e->getMessage()]);
}
}
}

+ 180
- 0
app/controller/Staff.php View File

@ -0,0 +1,180 @@
<?php
namespace app\controller;
use app\BaseController;
use app\model\Staff as StaffModel;
use app\model\Order as OrderModel;
use app\model\Product;
use app\service\CardService;
class Staff extends BaseController
{
protected $middleware = [
'StaffAuth' => ['except' => ['login']],
];
public function login()
{
$username = $this->request->post('username', '');
$password = $this->request->post('password', '');
$staff = StaffModel::where('username', $username)
->where('status', 1)
->find();
if (!$staff || !password_verify($password, $staff->password)) {
return json(['code' => -1, 'data' => null, 'msg' => '账号或密码错误']);
}
$secret = config('app.app_secret', 'bar_order_secret_key_2026');
$expire = time() + 86400;
$sign = hash_hmac('sha256', $staff->id . '|' . $expire, $secret);
$token = base64_encode($staff->id . '|' . $expire . '|' . $sign);
$staff->last_login = date('Y-m-d H:i:s');
$staff->save();
return json([
'code' => 0,
'data' => [
'token' => $token,
'nickname' => $staff->nickname,
'staffId' => $staff->id,
],
'msg' => '登录成功',
]);
}
public function orders()
{
$status = $this->request->get('status', 0);
$orders = OrderModel::with('items')
->where('status', intval($status))
->order('remind_count', 'desc')
->order('submitted_at', 'asc')
->select()
->toArray();
$list = array_map(function ($o) {
return [
'id' => $o['id'],
'orderNo' => $o['order_no'],
'cardNo' => $o['card_no'],
'status' => $o['status'],
'note' => $o['note'] ?? '',
'remindCount' => $o['remind_count'] ?? 0,
'submittedAt' => date('H:i', strtotime($o['submitted_at'])),
'items' => array_map(function ($i) {
return [
'name' => $i['product_name'],
'emoji' => $i['emoji'] ?? '',
'alc' => 0.0,
'qty' => $i['quantity'],
];
}, $o['items'] ?? []),
];
}, $orders);
return json(['code' => 0, 'data' => $list, 'msg' => 'ok']);
}
public function detail()
{
$id = $this->request->get('id', 0);
$order = OrderModel::with('items')->find(intval($id));
if (!$order) {
return json(['code' => -1, 'data' => null, 'msg' => '订单不存在']);
}
$items = [];
foreach ($order->items as $item) {
$product = Product::find($item->product_id);
$items[] = [
'name' => $item->product_name,
'emoji' => $item->emoji ?? '',
'alc' => $product ? floatval($product->alc) : 0.0,
'qty' => $item->quantity,
'recipe' => $product ? $product->recipe : '',
];
}
return json([
'code' => 0,
'data' => [
'id' => $order->id,
'orderNo' => $order->order_no,
'cardNo' => $order->card_no,
'status' => $order->status,
'note' => $order->note ?? '',
'remindCount' => $order->remind_count ?? 0,
'submittedAt' => date('H:i', strtotime($order->submitted_at)),
'items' => $items,
],
'msg' => 'ok',
]);
}
// BUG-02: 增加状态机校验 — confirm仅允许 status=0
public function confirm()
{
$id = $this->request->param('id', 0);
$order = OrderModel::find(intval($id));
if (!$order) {
return json(['code' => -1, 'data' => null, 'msg' => '订单不存在']);
}
if ($order->status !== 0) {
return json(['code' => -1, 'data' => null, 'msg' => '仅新订单可接单']);
}
$order->status = 1;
$order->remind_count = 0;
$order->staff_id = $this->request->staffId ?? null;
$order->handled_at = date('Y-m-d H:i:s');
$order->save();
return json(['code' => 0, 'data' => null, 'msg' => '已接单']);
}
// BUG-02: 增加状态机校验 — done仅允许 status=1
public function done(CardService $cardService)
{
$id = $this->request->param('id', 0);
$order = OrderModel::find(intval($id));
if (!$order) {
return json(['code' => -1, 'data' => null, 'msg' => '订单不存在']);
}
if ($order->status !== 1) {
return json(['code' => -1, 'data' => null, 'msg' => '仅进行中订单可结单']);
}
$order->status = 2;
$order->save();
$released = $cardService->release($order->card_no);
return json([
'code' => 0,
'data' => ['released' => $released],
'msg' => '已结单',
]);
}
// BUG-02: 增加状态机校验 — cancel仅允许 status=0
public function cancel()
{
$id = $this->request->param('id', 0);
$order = OrderModel::find(intval($id));
if (!$order) {
return json(['code' => -1, 'data' => null, 'msg' => '订单不存在']);
}
if ($order->status !== 0) {
return json(['code' => -1, 'data' => null, 'msg' => '仅新订单可取消']);
}
$order->status = 3;
$order->save();
return json(['code' => 0, 'data' => null, 'msg' => '已取消']);
}
}

+ 17
- 0
app/event.php View File

@ -0,0 +1,17 @@
<?php
// 事件定义文件
return [
'bind' => [
],
'listen' => [
'AppInit' => [],
'HttpRun' => [],
'HttpEnd' => [],
'LogLevel' => [],
'LogWrite' => [],
],
'subscribe' => [
],
];

+ 10
- 0
app/middleware.php View File

@ -0,0 +1,10 @@
<?php
// 全局中间件定义文件
return [
// 全局请求缓存
// \think\middleware\CheckRequestCache::class,
// 多语言加载
// \think\middleware\LoadLangPack::class,
// Session初始化
// \think\middleware\SessionInit::class
];

+ 47
- 0
app/middleware/StaffAuth.php View File

@ -0,0 +1,47 @@
<?php
namespace app\middleware;
use think\facade\Config;
class StaffAuth
{
public function handle($request, \Closure $next)
{
$token = $request->header('Authorization', '');
if (strpos($token, 'Bearer ') === 0) {
$token = substr($token, 7);
}
if (empty($token)) {
return json(['code' => -1, 'msg' => '请登录', 'data' => null])->code(401);
}
// 解析Token: base64(staff_id|expire|hmac)
$plain = base64_decode($token);
if (!$plain) {
return json(['code' => -1, 'msg' => 'Token无效', 'data' => null])->code(401);
}
$parts = explode('|', $plain);
if (count($parts) !== 3) {
return json(['code' => -1, 'msg' => 'Token格式错误', 'data' => null])->code(401);
}
[$staffId, $expire, $sign] = $parts;
// 校验过期
if (time() > intval($expire)) {
return json(['code' => -1, 'msg' => '登录已过期', 'data' => null])->code(401);
}
// 校验签名
$secret = Config::get('app.app_secret', 'bar_order_secret_key_2026');
$expected = hash_hmac('sha256', $staffId . '|' . $expire, $secret);
if (!hash_equals($expected, $sign)) {
return json(['code' => -1, 'msg' => 'Token签名错误', 'data' => null])->code(401);
}
$request->staffId = intval($staffId);
return $next($request);
}
}

+ 13
- 0
app/model/Message.php View File

@ -0,0 +1,13 @@
<?php
namespace app\model;
use think\Model;
class Message extends Model
{
protected $table = 'messages';
protected $pk = 'id';
protected $autoWriteTimestamp = true;
protected $createTime = 'created_at';
protected $updateTime = false;
}

+ 15
- 0
app/model/Order.php View File

@ -0,0 +1,15 @@
<?php
namespace app\model;
use think\Model;
class Order extends Model
{
protected $table = 'orders';
protected $pk = 'id';
public function items()
{
return $this->hasMany(OrderItem::class, 'order_id', 'id');
}
}

+ 15
- 0
app/model/OrderItem.php View File

@ -0,0 +1,15 @@
<?php
namespace app\model;
use think\Model;
class OrderItem extends Model
{
protected $table = 'order_items';
protected $pk = 'id';
public function order()
{
return $this->belongsTo(Order::class, 'order_id', 'id');
}
}

+ 20
- 0
app/model/Product.php View File

@ -0,0 +1,20 @@
<?php
namespace app\model;
use think\Model;
class Product extends Model
{
protected $table = 'products';
protected $pk = 'id';
public function scopeStatus($query)
{
return $query->where('status', 1);
}
public function scopeByCategory($query, $cate)
{
return $query->where('category', $cate)->order('sort_order', 'asc');
}
}

+ 10
- 0
app/model/Staff.php View File

@ -0,0 +1,10 @@
<?php
namespace app\model;
use think\Model;
class Staff extends Model
{
protected $table = 'staff';
protected $pk = 'id';
}

+ 9
- 0
app/provider.php View File

@ -0,0 +1,9 @@
<?php
use app\ExceptionHandle;
use app\Request;
// 容器Provider定义文件
return [
'think\Request' => Request::class,
'think\exception\Handle' => ExceptionHandle::class,
];

+ 9
- 0
app/service.php View File

@ -0,0 +1,9 @@
<?php
use app\AppService;
// 系统服务定义文件
// 服务在完成全局初始化之后执行
return [
AppService::class,
];

+ 41
- 0
app/service/CardService.php View File

@ -0,0 +1,41 @@
<?php
namespace app\service;
use app\model\Order;
class CardService
{
// 生成 [A-Z][0-9]{3} 格式号码牌,最多重试10次避免冲突
public function generate(): string
{
$maxRetry = 10;
for ($i = 0; $i < $maxRetry; $i++) {
$letter = chr(rand(65, 90)); // A-Z
$num = str_pad(rand(0, 999), 3, '0', STR_PAD_LEFT);
$cardNo = $letter . $num;
if ($this->isAvailable($cardNo)) {
return $cardNo;
}
}
// 冲突过多时追加随机码
return chr(rand(65, 90)) . str_pad(rand(0, 999), 3, '0', STR_PAD_LEFT) . rand(0, 9);
}
// 检查号码牌是否可用(不存在进行中/新单订单)
public function isAvailable(string $cardNo): bool
{
$count = Order::where('card_no', $cardNo)
->whereIn('status', [0, 1])
->count();
return $count === 0;
}
// 释放号码牌:检查该cardNo是否还有未完成订单
public function release(string $cardNo): bool
{
$count = Order::where('card_no', $cardNo)
->whereIn('status', [0, 1])
->count();
return $count === 0; // true=已释放,false=仍占用
}
}

+ 122
- 0
app/service/OrderService.php View File

@ -0,0 +1,122 @@
<?php
namespace app\service;
use app\model\Order;
use app\model\OrderItem;
use app\model\Product;
use think\facade\Db;
class OrderService
{
// 下单事务 (BUG-04: 增加数量校验)
public function submit(string $cardNo, array $items, string $note = ''): array
{
return Db::transaction(function () use ($cardNo, $items, $note) {
$totalAmount = 0;
$orderItems = [];
foreach ($items as $item) {
// BUG-04: 校验 qty 为正整数
$qty = intval($item['qty'] ?? 1);
if ($qty < 1 || $qty > 99) {
throw new \Exception('商品数量不合法');
}
$product = Product::find($item['productId']);
if (!$product || $product->status != 1) {
throw new \Exception('商品不存在或已下架');
}
$subtotal = $product->price * $qty;
$totalAmount += $subtotal;
$orderItems[] = [
'product_id' => $product->id,
'product_name' => $product->name,
'emoji' => $product->emoji ?? '',
'price' => $product->price,
'quantity' => $qty,
];
}
if ($totalAmount <= 0) {
throw new \Exception('订单金额无效');
}
// BUG-03 (旧): 订单号增加秒级精度避免并发冲突
$orderNo = 'OD' . date('His') . substr($cardNo, -3) . rand(10, 99);
$order = Order::create([
'order_no' => $orderNo,
'card_no' => $cardNo,
'total_amount' => $totalAmount,
'status' => 0,
'note' => $note,
'submitted_at' => date('Y-m-d H:i:s'),
]);
foreach ($orderItems as &$oi) {
$oi['order_id'] = $order->id;
}
(new OrderItem())->saveAll($orderItems);
return ['orderNo' => $order->order_no];
});
}
// 顾客订单列表
public function listByCard(string $cardNo): array
{
$orders = Order::with('items')
->where('card_no', $cardNo)
->order('submitted_at', 'desc')
->select()
->toArray();
return array_map(function ($o) {
return [
'id' => $o['id'],
'orderNo' => $o['order_no'],
'cardNo' => $o['card_no'],
'status' => $o['status'],
'note' => $o['note'] ?? '',
'remindCount' => $o['remind_count'] ?? 0,
'submittedAt' => date('H:i', strtotime($o['submitted_at'])),
'items' => array_map(function ($i) {
return [
'name' => $i['product_name'],
'emoji' => $i['emoji'] ?? '',
'alc' => 0.0,
'qty' => $i['quantity'],
];
}, $o['items'] ?? []),
];
}, $orders);
}
// BUG-01: 催单增加归属校验 + 状态校验 + 频率限制
public function remind(int $orderId, string $cardNo): void
{
$order = Order::find($orderId);
if (!$order) {
throw new \Exception('订单不存在');
}
// 归属校验
if ($order->card_no !== $cardNo) {
throw new \Exception('无权操作此订单');
}
// 状态校验:仅新单可催
if ($order->status !== 0) {
throw new \Exception('当前订单状态不可催单');
}
// 频率限制:30秒冷却
$lastRemind = strtotime($order->updated_at ?? $order->submitted_at);
if (time() - $lastRemind < 30) {
throw new \Exception('催单太频繁,请30秒后再试');
}
// 上限:最多5次
if ($order->remind_count >= 5) {
throw new \Exception('催单次数已达上限');
}
Order::where('id', $orderId)->inc('remind_count', 1)->update();
}
}

+ 49
- 0
composer.json View File

@ -0,0 +1,49 @@
{
"name": "topthink/think",
"description": "the new thinkphp framework",
"type": "project",
"keywords": [
"framework",
"thinkphp",
"ORM"
],
"homepage": "https://www.thinkphp.cn/",
"license": "Apache-2.0",
"authors": [
{
"name": "liu21st",
"email": "liu21st@gmail.com"
},
{
"name": "yunwuxin",
"email": "448901948@qq.com"
}
],
"require": {
"php": ">=8.0.0",
"topthink/framework": "^8.0",
"topthink/think-orm": "^3.0|^4.0",
"topthink/think-filesystem": "^2.0|^3.0"
},
"require-dev": {
"topthink/think-dumper": "^1.0",
"topthink/think-trace": "^2.0"
},
"autoload": {
"psr-4": {
"app\\": "app"
},
"psr-0": {
"": "extend/"
}
},
"config": {
"preferred-install": "dist"
},
"scripts": {
"post-autoload-dump": [
"@php think service:discover",
"@php think vendor:publish"
]
}
}

+ 14
- 0
config/app.php View File

@ -0,0 +1,14 @@
<?php
return [
'app_namespace' => '',
'with_route' => true,
'default_app' => 'index',
'default_timezone' => 'Asia/Shanghai',
'app_map' => [],
'domain_bind' => [],
'deny_app_list' => [],
'app_secret' => 'bar_order_secret_key_2026',
'exception_tmpl' => app()->getThinkPath() . 'tpl/think_exception.tpl',
'error_message' => '页面错误!请稍后再试~',
'show_error_msg' => false,
];

+ 29
- 0
config/cache.php View File

@ -0,0 +1,29 @@
<?php
// +----------------------------------------------------------------------
// | 缓存设置
// +----------------------------------------------------------------------
return [
// 默认缓存驱动
'default' => 'file',
// 缓存连接方式配置
'stores' => [
'file' => [
// 驱动方式
'type' => 'File',
// 缓存保存目录
'path' => '',
// 缓存前缀
'prefix' => '',
// 缓存有效期 0表示永久缓存
'expire' => 0,
// 缓存标签前缀
'tag_prefix' => 'tag:',
// 序列化机制 例如 ['serialize', 'unserialize']
'serialize' => [],
],
// 更多的缓存连接
],
];

+ 9
- 0
config/console.php View File

@ -0,0 +1,9 @@
<?php
// +----------------------------------------------------------------------
// | 控制台配置
// +----------------------------------------------------------------------
return [
// 指令定义
'commands' => [
],
];

+ 20
- 0
config/cookie.php View File

@ -0,0 +1,20 @@
<?php
// +----------------------------------------------------------------------
// | Cookie设置
// +----------------------------------------------------------------------
return [
// cookie 保存时间
'expire' => 0,
// cookie 保存路径
'path' => '/',
// cookie 有效域名
'domain' => '',
// cookie 启用安全传输
'secure' => false,
// httponly设置
'httponly' => false,
// 是否使用 setcookie
'setcookie' => true,
// samesite 设置,支持 'strict' 'lax'
'samesite' => '',
];

+ 31
- 0
config/database.php View File

@ -0,0 +1,31 @@
<?php
return [
'default' => env('DB_TYPE', 'mysql'),
'time_query_rule' => [],
'auto_timestamp' => true,
'datetime_format' => 'Y-m-d H:i:s',
'datetime_field' => '',
'connections' => [
'mysql' => [
'type' => 'mysql',
'hostname' => env('DB_HOST', '127.0.0.1'),
'database' => env('DB_NAME', 'bar_order'),
'username' => env('DB_USER', 'root'),
'password' => env('DB_PASS', ''),
'hostport' => env('DB_PORT', '3306'),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'prefix' => env('DB_PREFIX', ''),
'deploy' => 0,
'rw_separate' => false,
'master_num' => 1,
'fields_strict' => true,
'trigger_sql' => env('APP_DEBUG', true),
'fields_cache' => false,
],
'sqlite' => [
'type' => 'sqlite',
'database' => env('DB_NAME', 'runtime/bar_order.db'),
'prefix' => '',
],
],
];

+ 24
- 0
config/filesystem.php View File

@ -0,0 +1,24 @@
<?php
return [
// 默认磁盘
'default' => 'local',
// 磁盘列表
'disks' => [
'local' => [
'type' => 'local',
'root' => app()->getRuntimePath() . 'storage',
],
'public' => [
// 磁盘类型
'type' => 'local',
// 磁盘路径
'root' => app()->getRootPath() . 'public/storage',
// 磁盘路径对应的外部URL路径
'url' => '/storage',
// 可见性
'visibility' => 'public',
],
// 更多的磁盘配置信息
],
];

+ 29
- 0
config/lang.php View File

@ -0,0 +1,29 @@
<?php
// +----------------------------------------------------------------------
// | 多语言设置
// +----------------------------------------------------------------------
return [
// 默认语言
'default_lang' => env('DEFAULT_LANG', 'zh-cn'),
// 自动侦测浏览器语言
'auto_detect_browser' => true,
// 允许的语言列表
'allow_lang_list' => [],
// 多语言自动侦测变量名
'detect_var' => 'lang',
// 是否使用Cookie记录
'use_cookie' => true,
// 多语言cookie变量
'cookie_var' => 'think_lang',
// 多语言header变量
'header_var' => 'think-lang',
// 扩展语言包
'extend_list' => [],
// Accept-Language转义为对应语言包名称
'accept_language' => [
'zh-hans-cn' => 'zh-cn',
],
// 是否支持语言分组
'allow_group' => false,
];

+ 45
- 0
config/log.php View File

@ -0,0 +1,45 @@
<?php
// +----------------------------------------------------------------------
// | 日志设置
// +----------------------------------------------------------------------
return [
// 默认日志记录通道
'default' => 'file',
// 日志记录级别
'level' => [],
// 日志类型记录的通道 ['error'=>'email',...]
'type_channel' => [],
// 关闭全局日志写入
'close' => false,
// 全局日志处理 支持闭包
'processor' => null,
// 日志通道列表
'channels' => [
'file' => [
// 日志记录方式
'type' => 'File',
// 日志保存目录
'path' => '',
// 单文件日志写入
'single' => false,
// 独立日志级别
'apart_level' => [],
// 最大日志文件数量
'max_files' => 0,
// 使用JSON格式记录
'json' => false,
// 日志处理
'processor' => null,
// 关闭通道日志写入
'close' => false,
// 日志输出格式化
'format' => '[%s][%s] %s',
// 是否实时写入
'realtime_write' => false,
],
// 其它日志通道配置
],
];

+ 7
- 0
config/middleware.php View File

@ -0,0 +1,7 @@
<?php
return [
'alias' => [
'StaffAuth' => app\middleware\StaffAuth::class,
],
'priority' => [],
];

+ 55
- 0
config/route.php View File

@ -0,0 +1,55 @@
<?php
// +----------------------------------------------------------------------
// | 路由设置
// +----------------------------------------------------------------------
return [
// pathinfo分隔符
'pathinfo_depr' => '/',
// 是否开启路由延迟解析
'url_lazy_route' => false,
// 是否强制使用路由
'url_route_must' => false,
// 是否区分大小写
'url_case_sensitive' => false,
// 自动扫描子目录分组
'route_auto_group' => false,
// 合并路由规则
'route_rule_merge' => false,
// 路由是否完全匹配
'route_complete_match' => false,
// 去除斜杠
'remove_slash' => false,
// 默认的路由变量规则
'default_route_pattern' => '[\w\.]+',
// URL伪静态后缀
'url_html_suffix' => 'html',
// 访问控制器层名称
'controller_layer' => 'controller',
// 空控制器名
'empty_controller' => 'Error',
// 是否使用控制器后缀
'controller_suffix' => false,
// 默认模块名(开启自动多模块有效)
'default_module' => 'index',
// 默认控制器名
'default_controller' => 'Index',
// 默认操作名
'default_action' => 'index',
// 操作方法后缀
'action_suffix' => '',
// 非路由变量是否使用普通参数方式(用于URL生成)
'url_common_param' => true,
// 操作方法的参数绑定方式 route get param
'action_bind_param' => 'get',
// 请求缓存规则 true为自动规则
'request_cache_key' => true,
// 请求缓存有效期
'request_cache_expire' => null,
// 全局请求缓存排除规则
'request_cache_except' => [],
// 请求缓存的Tag
'request_cache_tag' => '',
// API版本header变量
'api_version' => 'Api-Version',
];

+ 19
- 0
config/session.php View File

@ -0,0 +1,19 @@
<?php
// +----------------------------------------------------------------------
// | 会话设置
// +----------------------------------------------------------------------
return [
// session name
'name' => 'PHPSESSID',
// SESSION_ID的提交变量,解决flash上传跨域
'var_session_id' => '',
// 驱动方式 支持file cache
'type' => 'file',
// 存储连接标识 当type使用cache的时候有效
'store' => null,
// 过期时间
'expire' => 1440,
// 前缀
'prefix' => '',
];

+ 10
- 0
config/trace.php View File

@ -0,0 +1,10 @@
<?php
// +----------------------------------------------------------------------
// | Trace设置 开启调试模式后有效
// +----------------------------------------------------------------------
return [
// 内置Html和Console两种方式 支持扩展
'type' => 'Html',
// 读取的日志通道名
'channel' => '',
];

+ 25
- 0
config/view.php View File

@ -0,0 +1,25 @@
<?php
// +----------------------------------------------------------------------
// | 模板设置
// +----------------------------------------------------------------------
return [
// 模板引擎类型使用Think
'type' => 'Think',
// 默认模板渲染规则 1 解析为小写+下划线 2 全部转换小写 3 保持操作方法
'auto_rule' => 1,
// 模板目录名
'view_dir_name' => 'view',
// 模板后缀
'view_suffix' => 'html',
// 模板文件名分隔符
'view_depr' => DIRECTORY_SEPARATOR,
// 模板引擎普通标签开始标记
'tpl_begin' => '{',
// 模板引擎普通标签结束标记
'tpl_end' => '}',
// 标签库标签开始标记
'taglib_begin' => '{',
// 标签库标签结束标记
'taglib_end' => '}',
];

+ 2
- 0
extend/.gitignore View File

@ -0,0 +1,2 @@
*
!.gitignore

+ 8
- 0
public/.htaccess View File

@ -0,0 +1,8 @@
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L]
</IfModule>

+ 0
- 0
public/drink View File


BIN
public/favicon.ico View File

Before After
Width: 176  |  Height: 197  |  Size: 5.3 KiB

+ 25
- 0
public/index.php View File

@ -0,0 +1,25 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
use think\App;
// [ 应用入口文件 ]
require __DIR__ . '/../vendor/autoload.php';
// 执行HTTP应用并响应
$http = (new App())->http;
$response = $http->run();
$response->send();
$http->end($response);

+ 2
- 0
public/robots.txt View File

@ -0,0 +1,2 @@
User-agent: *
Disallow:

+ 19
- 0
public/router.php View File

@ -0,0 +1,19 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2019 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
// $Id$
if (is_file($_SERVER["DOCUMENT_ROOT"] . $_SERVER["SCRIPT_NAME"])) {
return false;
} else {
$_SERVER["SCRIPT_FILENAME"] = __DIR__ . '/index.php';
require __DIR__ . "/index.php";
}

+ 2
- 0
public/static/.gitignore View File

@ -0,0 +1,2 @@
*
!.gitignore

+ 24
- 0
route/app.php View File

@ -0,0 +1,24 @@
<?php
use think\facade\Route;
// ── 顾客端(无需认证) ──
Route::post('api/card/generate', 'Card/generate');
Route::get('api/card/check', 'Card/check');
Route::get('api/menu/categories', 'Menu/categories');
Route::get('api/menu/products', 'Menu/products');
Route::get('api/menu/product', 'Menu/detail');
Route::post('api/order/submit', 'Order/submit');
Route::get('api/order/list', 'Order/list');
Route::post('api/order/remind', 'Order/remind');
Route::get('api/message/list', 'Message/list');
Route::post('api/message/send', 'Message/send');
// ── 员工端(需Token中间件) ──
Route::post('api/staff/login', 'Staff/login');
Route::group('api/staff', function () {
Route::get('orders', 'Staff/orders');
Route::get('order', 'Staff/detail');
Route::post('order/confirm', 'Staff/confirm');
Route::post('order/done', 'Staff/done');
Route::post('order/cancel', 'Staff/cancel');
})->middleware('StaffAuth');

+ 11
- 0
think View File

@ -0,0 +1,11 @@
#!/usr/bin/env php
<?php
use think\App;
// 命令行入口文件
// 加载基础文件
require __DIR__ . '/vendor/autoload.php';
// 应用初始化
(new App())->console->run();

+ 1
- 0
view/README.md View File

@ -0,0 +1 @@
如果不使用模板,可以删除该目录

Loading…
Cancel
Save