分类 默认分类 下的文章

我来介绍 ThinkPHP 8 实现一个安全的 API 的完整流程:

1. 首先创建项目并安装必要依赖

composer create-project topthink/think tp8
cd tp8
composer require topthink/think-migration

2. 创建数据表迁移文件

php think make:migration create_users_table

php:database/migrations/xxx_create_users_table.php

<?php
use think\migration\Migrator;

class CreateUsersTable extends Migrator
{
    public function change()
    {
        $this->table('users')
            ->addColumn('username', 'string', ['limit' => 50])
            ->addColumn('password', 'string', ['limit' => 255])
            ->addColumn('api_token', 'string', ['limit' => 100, 'null' => true])
            ->addColumn('status', 'boolean', ['default' => 1])
            ->addColumn('create_time', 'timestamp', ['null' => true])
            ->addColumn('update_time', 'timestamp', ['null' => true])
            ->create();
    }
}

3. 创建验证器
php:app/validate/User.php

<?php
namespace app\validate;

use think\Validate;

class User extends Validate
{
    protected $rule = [
        'username' => 'require|min:3|max:50',
        'password' => 'require|min:6',
    ];
    
    protected $message = [
        'username.require' => '用户名不能为空',
        'username.min'    => '用户名至少3个字符',
        'password.require' => '密码不能为空',
        'password.min'    => '密码至少6个字符',
    ];
}

4. 创建中间件进行 Token 验证
php:app/middleware/CheckApiToken.php

<?php
namespace app\middleware;

class CheckApiToken
{
    public function handle($request, \Closure $next)
    {
        $token = $request->header('Authorization');
        
        if (!$token) {
            return json(['code' => 401, 'msg' => '未授权访问']);
        }
        
        // 验证 token
        $user = \app\model\User::where('api_token', $token)->find();
        if (!$user) {
            return json(['code' => 401, 'msg' => '无效的 token']);
        }
        
        // 将用户信息注入请求中
        $request->user = $user;
        
        return $next($request);
    }
}

5. 创建控制器
php:app/controller/Api.php

<?php
namespace app\controller;

use app\BaseController;
use app\model\User;
use think\facade\Validate;

class Api extends BaseController
{
    // 注册接口
    public function register()
    {
        $data = $this->request->post();
        
        // 验证数据
        $validate = validate('User');
        if (!$validate->check($data)) {
            return json(['code' => 422, 'msg' => $validate->getError()]);
        }
        
        // 创建用户
        $user = new User;
        $user->username = $data['username'];
        $user->password = password_hash($data['password'], PASSWORD_DEFAULT);
        $user->api_token = md5(uniqid() . time());
        $user->save();
        
        return json([
            'code' => 200,
            'msg' => '注册成功',
            'data' => [
                'token' => $user->api_token
            ]
        ]);
    }
    
    // 需要验证 token 的接口
    public function getUserInfo()
    {
        // 用户信息已经在中间件中注入
        $user = $this->request->user;
        
        return json([
            'code' => 200,
            'data' => [
                'username' => $user->username,
                'create_time' => $user->create_time
            ]
        ]);
    }
}

6. 配置路由
php:route/api.php

<?php
use think\facade\Route;

// 公开接口
Route::post('api/register', 'Api/register');

// 需要验证 token 的接口组
Route::group('api', function () {
    Route::get('user/info', 'Api/getUserInfo');
})->middleware(\app\middleware\CheckApiToken::class);

7. 配置跨域和请求频率限制(可选)
php:config/middleware.php

return [
    // 别名或分组
    'alias'    => [
        'throttle' => \think\middleware\Throttle::class,
        'cors'     => \think\middleware\AllowCrossDomain::class,
    ],
    // 优先级设置,此数组中的中间件会按照数组中的顺序优先执行
    'priority' => [
        \think\middleware\AllowCrossDomain::class,
        \think\middleware\Throttle::class,
    ],
];

主要安全特性:

  1. 数据验证:使用验证器进行输入验证
  2. Token 认证:通过中间件验证 API token
  3. 密码加密:使用 password_hash 加密存储密码
  4. 频率限制:可选的请求频率限制
  5. 跨域保护:可配置跨域规则

使用示例:

  1. 首先注册获取 token:
curl -X POST http://your-domain/api/register \
  -H "Content-Type: application/json" \
  -d '{"username":"test","password":"123456"}'
  1. 使用 token 访问需要认证的接口:
curl http://your-domain/api/user/info \
  -H "Authorization: YOUR_TOKEN_HERE"

这样就实现了一个基本的、安全的 API 框架。可以基于这个框架继续扩展其他功能。

建议还可以添加:

  1. 日志记录
  2. 统一的异常处理
  3. 接口版本控制
  4. 缓存机制
  5. 接口文档生成

实现上面5点

我来详细说明这五个功能的实现方法:

1. 日志记录

首先在 .env 文件配置日志:

[LOG]
CHANNEL = file
LEVEL = info

创建日志中间件:
php:app/middleware/ApiLog.php

<?php
namespace app\middleware;

use think\facade\Log;

class ApiLog
{
    public function handle($request, \Closure $next)
    {
        // 记录请求信息
        $startTime = microtime(true);
        
        // 记录请求数据
        Log::info('API请求', [
            'url'      => $request->url(true),
            'method'   => $request->method(),
            'ip'       => $request->ip(),
            'params'   => $request->param(),
            'header'   => $request->header()
        ]);
        
        $response = $next($request);
        
        // 记录响应数据
        Log::info('API响应', [
            'duration' => round((microtime(true) - $startTime) * 1000, 2) . 'ms',
            'response' => $response->getData()
        ]);
        
        return $response;
    }
}

2. 统一的异常处理

创建异常处理类:
php:app/ExceptionHandle.php

<?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
{
    private $code = 500;
    
    public function render($request, Throwable $e): Response
    {
        // 添加自定义异常处理机制
        if ($e instanceof ValidateException) {
            $this->code = 422;
            $msg = $e->getError();
        } elseif ($e instanceof ModelNotFoundException || $e instanceof DataNotFoundException) {
            $this->code = 404;
            $msg = '数据不存在';
        } elseif ($e instanceof HttpException) {
            $this->code = $e->getStatusCode();
            $msg = $e->getMessage();
        } else {
            $msg = $e->getMessage();
        }
        
        // 开发环境显示详细错误信息
        $debug = [];
        if (env('APP_DEBUG')) {
            $debug = [
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'trace' => $e->getTraceAsString()
            ];
        }
        
        return json([
            'code' => $this->code,
            'msg'  => $msg,
            'debug' => $debug
        ]);
    }
}

3. 接口版本控制

创建版本控制中间件:
php:app/middleware/ApiVersion.php

<?php
namespace app\middleware;

class ApiVersion
{
    public function handle($request, \Closure $next)
    {
        // 从 header 获取版本号
        $version = $request->header('api-version');
        
        // 设置默认版本
        if (!$version) {
            $version = 'v1';
        }
        
        // 将版本信息注入到请求中
        $request->version = $version;
        
        return $next($request);
    }
}

配置路由:
php:route/api.php

<?php
use think\facade\Route;

// v1 版本接口
Route::group('api/v1', function () {
    Route::get('user/info', 'api.v1.User/info');
})->middleware([
    \app\middleware\ApiVersion::class,
    \app\middleware\CheckApiToken::class
]);

// v2 版本接口
Route::group('api/v2', function () {
    Route::get('user/info', 'api.v2.User/info');
})->middleware([
    \app\middleware\ApiVersion::class,
    \app\middleware\CheckApiToken::class
]);

4. 缓存机制

配置缓存:
php:config/cache.php

return [
    'default' => env('cache.driver', 'redis'),
    'stores'  => [
        'redis' => [
            'type'   => 'redis',
            'host'   => env('redis.host', '127.0.0.1'),
            'port'   => env('redis.port', 6379),
            'prefix' => env('cache.prefix', 'think_'),
        ],
    ],
];

使用示例:
php:app/controller/Api.php

use think\facade\Cache;

public function getUserInfo()
{
    $userId = $this->request->user->id;
    $cacheKey = "user_info_{$userId}";
    
    // 尝试从缓存获取
    $userInfo = Cache::get($cacheKey);
    if (!$userInfo) {
        // 从数据库获取
        $userInfo = User::find($userId);
        // 存入缓存,设置过期时间为1小时
        Cache::set($cacheKey, $userInfo, 3600);
    }
    
    return json(['code' => 200, 'data' => $userInfo]);
}

5. 接口文档生成

使用 swagger-php 生成接口文档:

首先安装:

composer require zircote/swagger-php

在控制器中添加注解:
php:app/controller/Api.php

<?php
namespace app\controller;

/**
 * @OA\Info(
 *     title="API文档",
 *     version="1.0.0"
 * )
 */
class Api extends BaseController
{
    /**
     * @OA\Post(
     *     path="/api/register",
     *     summary="用户注册",
     *     @OA\RequestBody(
     *         @OA\JsonContent(
     *             required={"username","password"},
     *             @OA\Property(property="username", type="string"),
     *             @OA\Property(property="password", type="string"),
     *         )
     *     ),
     *     @OA\Response(
     *         response=200,
     *         description="注册成功"
     *     )
     * )
     */
    public function register()
    {
        // 实现代码...
    }
}

创建生成文档的命令:
php:app/command/SwaggerDoc.php

<?php
namespace app\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;
use OpenApi\Generator;

class SwaggerDoc extends Command
{
    protected function configure()
    {
        $this->setName('swagger:gen')
            ->setDescription('生成Swagger文档');
    }

    protected function execute(Input $input, Output $output)
    {
        $openapi = Generator::scan([app_path()]);
        file_put_contents(public_path() . 'swagger.json', $openapi->toJson());
        $output->writeln('文档生成成功!');
    }
}

生成文档:

php think swagger:gen

然后在 public 目录下创建 swagger-ui 页面:
html:public/docs.html

<!DOCTYPE html>
<html>
<head>
    <title>API文档</title>
    <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@latest/swagger-ui.css">
</head>
<body>
    <div id="swagger-ui"></div>
    <script src="https://unpkg.com/swagger-ui-dist@latest/swagger-ui-bundle.js"></script>
    <script>
        window.onload = function() {
            SwaggerUIBundle({
                url: "/swagger.json",
                dom_id: '#swagger-ui'
            });
        }
    </script>
</body>
</html>

访问 http://your-domain/docs.html 即可查看API文档。

整合到路由中:
php:route/api.php

<?php
use think\facade\Route;

Route::group('api', function () {
    // 路由定义...
})->middleware([
    \app\middleware\ApiLog::class,      // 日志记录
    \app\middleware\ApiVersion::class,   // 版本控制
    \app\middleware\CheckApiToken::class // token验证
]);

这样就实现了一个相对完整的 API 框架,包含:

  • 详细的日志记录
  • 统一的异常处理
  • API 版本控制
  • 缓存支持
  • 自动生成的 API 文档

根据实际项目需求,可以选择性地实现这些功能,或者根据具体情况进行调整。

让我解释一下 @click="deleteAll"@click="deleteAll()" 的区别:

  1. 不加括号 @click="deleteAll"
<text class="delete" @click="deleteAll">清空</text>

// 这种写法会:
// 1. 自动传入事件对象 event 作为第一个参数
// 2. 当点击时才调用函数
function deleteAll(event) {
    console.log(event)  // 可以获取到事件对象
    // 执行清空逻辑
}
  1. 加括号 @click="deleteAll()"
<text class="delete" @click="deleteAll()">清空</text>

// 这种写法:
// 1. 不会自动传入事件对象
// 2. 当点击时调用函数
function deleteAll() {
    // 无法获取事件对象
    // 执行清空逻辑
}
  1. 如果需要同时获取事件对象和传参
<!-- 方法一:使用 $event -->
<text @click="deleteAll($event, otherParam)">清空</text>

<!-- 方法二:使用内联函数 -->
<text @click="(event) => deleteAll(event, otherParam)">清空</text>

function deleteAll(event, otherParam) {
    console.log(event)      // 事件对象
    console.log(otherParam) // 其他参数
}

在你的场景中:

  • 如果不需要使用事件对象,两种写法都可以
  • 如果需要事件对象(比如阻止冒泡),建议用不加括号的写法

建议:

  • 一般情况下不加括号更简洁
  • 需要传参时才加括号
  • 需要事件对象时不加括号

让我帮你理解 TypeScript 代码,通过对比来学习:

  1. 基础类型定义
// 没用 TS 时
let price = 88
let name = '红烧肉'

// 用了 TS 后
let price: number = 88         // 意思是:price 只能是数字
let name: string = '红烧肉'    // 意思是:name 只能是字符串
  1. 对象类型定义
// 没用 TS 时
const dish = {
    name: '红烧肉',
    price: 88,
    isSpicy: true
}

// 用了 TS 后
interface Dish {              // interface 相当于定义一个"模板"
    name: string;            // 规定 name 必须是字符串
    price: number;          // 规定 price 必须是数字
    isSpicy: boolean;      // 规定 isSpicy 必须是布尔值
}

const dish: Dish = {        // 告诉 TS:这个对象必须符合 Dish 的格式
    name: '红烧肉',
    price: 88,
    isSpicy: true
}
  1. 数组类型定义
// 没用 TS 时
const prices = [88, 66, 128]
const dishes = ['红烧肉', '青椒肉丝']

// 用了 TS 后
const prices: number[] = [88, 66, 128]     // 数组里只能放数字
const dishes: string[] = ['红烧肉', '青椒肉丝']  // 数组里只能放字符串
  1. 函数类型定义
// 没用 TS 时
function calculateTotal(price, quantity) {
    return price * quantity
}

// 用了 TS 后
function calculateTotal(price: number, quantity: number): number {
    return price * quantity
}
// 意思是:
// 1. price 必须是数字
// 2. quantity 必须是数字
// 3. 返回值必须是数字
  1. Vue 组件中的应用
// 没用 TS 时
const count = ref(0)
const message = ref('')

// 用了 TS 后
const count = ref<number>(0)      // count 只能是数字
const message = ref<string>('')   // message 只能是字符串
  1. 事件处理
// 没用 TS 时
function handleClick(event) {
    console.log(event.target.value)
}

// 用了 TS 后
function handleClick(event: MouseEvent): void {
    const target = event.target as HTMLInputElement  // 告诉 TS:这是一个输入框
    console.log(target.value)
}

学习建议:

  1. 循序渐进

    • 先理解基础类型(string, number, boolean)
    • 再学习对象和数组的类型定义
    • 最后学习接口和泛型
  2. 从简单开始
// 第一步:添加基础类型
let price: number = 88
let name: string = '红烧肉'

// 第二步:添加对象类型
interface MenuItem {
    name: string
    price: number
}

// 第三步:添加数组类型
const menu: MenuItem[] = [
    { name: '红烧肉', price: 88 },
    { name: '青椒肉丝', price: 66 }
]
  1. 使用 VS Code
  • 安装 TypeScript 插件
  • 鼠标悬停可以看到类型提示
  • 代码补全更智能
  1. 常见错误理解
// ❌ 错误示例
const price: number = "88"  // 错误:字符串不能赋值给数字类型

// ✅ 正确示例
const price: number = 88    // 正确:数字类型

记住:

  • TypeScript 主要是帮助我们避免错误
  • 类型定义就像是"约定"
  • 代码看起来复杂了,但更安全了
  • 开发时会有更好的提示

如果你觉得某段代码看不懂,可以:

  1. 去掉类型声明,看纯 JavaScript 逻辑
  2. 一点点加入类型,理解每个类型的作用
  3. 使用 VS Code 的类型提示功能

categoriesstate.categories 在 Vue3 中有重要区别:

  1. 响应式处理方式
// 直接定义
const categories = ref([...])  // 使用 ref
// 或
const categories = reactive([...])  // 使用 reactive

// 通过 state 定义
const state = reactive({
    categories: [...],
    otherData: ...
})
  1. 访问方式
// categories 的访问
categories.value  // 如果是 ref,需要 .value
// 或
categories  // 如果是 reactive,直接访问

// state.categories 的访问
state.categories  // 永远是直接访问,不需要 .value
  1. 数据组织
// categories 方式
const categories = reactive([...])
const currentIndex = ref(0)
const otherData = ref('')

// state 方式(推荐)
const state = reactive({
    categories: [...],
    currentIndex: 0,
    otherData: ''
})
  1. 代码示例
// 使用 categories
const categories = reactive([])
function updateCategories() {
    categories.push(newItem)  // 直接修改
}

// 使用 state.categories
const state = reactive({
    categories: []
})
function updateCategories() {
    state.categories.push(newItem)  // 通过 state 修改
}

使用 state.categories 的优势:

  1. 统一的数据管理
  2. 更清晰的数据结构
  3. 避免多个响应式变量散落
  4. 更容易进行状态管理

建议使用 state.categories 的方式,因为它更符合 Vue3 的最佳实践。

对比一下两种写法:

原始代码:

const checkedItemsCount = computed(() => {
    let total = 0
    categories.map(category => {
        total += category.items.filter(item => item.check).length;
    });
    return total
});

优化后的代码:

const checkedItemsCount = computed(() => {
    return state.categories.reduce((total, category) => 
        total + (category.items?.filter(item => item.check)?.length ?? 0), 0
    )
})

主要区别:

  1. 空值安全性
  • 原始代码:直接访问 category.items,如果 itemsundefined 会报错
  • 优化代码:使用 ?.?? 操作符处理可能的空值,更安全
  1. 性能差异
  • 原始代码:使用 map 但没有使用其返回值,实际上应该用 forEach
  • 优化代码:使用 reduce 直接进行累加,减少了中间变量
  1. 函数式编程
  • 原始代码:使用命令式编程,维护外部变量 total
  • 优化代码:使用函数式编程,无副作用,更容易测试和维护
  1. 代码健壮性
// 原始代码可能出错的情况:
categories = undefined  // 报错
category.items = undefined  // 报错

// 优化代码能处理的情况:
state.categories = undefined  // 返回 0
category.items = undefined  // 返回 0
  1. 内存使用
  • 原始代码:需要额外的 total 变量
  • 优化代码:直接在 reduce 过程中计算,无需额外变量

建议使用优化后的写法,因为它:

  • 更安全(处理了空值情况)
  • 更高效(减少了不必要的操作)
  • 更易维护(函数式编程风格)
  • 更简洁(代码更少但功能更强)