PHP转Go系列 | ThinkPHP与Gin框架之OpenApi授权设计实践
大家好,我是码农先森。
我之前待过一个做 ToB 业务的公司,主要是研发以会员为中心的 SaaS 平台,其中涉及的子系统有会员系统、积分系统、营销系统等。在这个 SaaS 平台中有一个重要的角色「租户」,这个租户可以拥有一个或多个子系统的使用权限,此外租户还可以使用平台所提供的开放 API 「即 OpenApi」来获取相关系统的数据。有了 OpenApi 租户可以更便捷的与租户自有系统进行打通,提高系统之间数据的传输效率。那么这一次实践的主要内容是 OpenApi 的授权设计,希望对大家能有所帮助。

我们先梳理一下本次实践的关键步骤:
- 给每一个租户分配一对 AppKey、AppSecret。
- 租户通过传递 AppKey、AppSecret 参数获取到平台颁发的 AccessToken。
- 租户再通过 AccessToken 来换取可以实际调用 API 的 RefreshToken。
- 这时的 RefreshToken 是具有时效性,目前设置的有效期为 2 个小时。
- 针对 RefreshToken 还会提供一个刷新时效的接口。
- 只有 RefreshToken 才有调用业务 API 的真实权限。
有些朋友对 AccessToken 和 RefreshToken 傻傻分不清,疑问重重?我在最开始接触这个设计的时候也是懵逼的,为啥要搞两个,一个不也能解决问题吗?确实搞一个也可以用,但大家如果对接过微信的开放 API 就会发现他们也是有两个,此外还有很多大的开放平台也是采用类似的设计逻辑,所以存在即合理。
这里我说一下具体的原因,AccessToken 是基于 AppKey 和 AppSecret 来生成的,而 RefreshToken 是通过 AccessToken 交换得来的。并且 RefreshToken 具备有效性,需要通过一个刷新接口,不定时的刷新 RefreshToken。RefreshToken 的使用是最频繁的,在每次的业务 API 调用是都需要进行传输,传输的次数多了那么 RefreshToken 被劫持的风险就会变大。假设 RefreshToken 真的被泄露,那么损失也是控制在 2 个小时以内,为了减低损失也还可以调低有效时间。总而言之,网络的传输并不总是能保证安全,AccessToken 在网络上只需要一次传输「即换取 RefreshToken」,而 RefreshToken 需要不断的在网络的传输「即不断调用业务 API」,传输的次数越少风险就越低,这就是设计两个 Token 的根本原因。
话不多说,开整!
按照惯例,我们先对整个目录结构进行梳理。这次的重点逻辑主要是在控制器 controller 的 auth 中实现,包含三个 API 接口一是生成 AccessToken、二是通过 AccessToken 交换 RefreshToken,三是刷新 RefreshToken。中间件 middleware 的 api_auth 是对 RefreshToken 进行解码验证,判断客户端传递的 RefreshToken 是否有效。此外,AccessToken 和 RefreshToken 的生成策略都是采用的 JWT 规则。
[manongsen@root php_to_go]$ tree -L 2
.
├── go_openapi
│ ├── app
│ │ ├── controller
│ │ │ ├── auth.go
│ │ │ └── user.go
│ │ ├── middleware
│ │ │ └── api_auth.go
│ │ ├── model
│ │ │ └── tenant.go
│ │ ├── config
│ │ │ └── config.go
│ │ └── route.go
│ ├── go.mod
│ ├── go.sum
│ └── main.go
└── php_openapi
│ ├── app
│ │ ├── controller
│ │ │ ├── Auth.php
│ │ │ └── User.php
│ │ ├── middleware
│ │ │ └── ApiAuth.php
│ │ ├── model
│ │ │ └── Tenant.php
│ │ └── middleware.php
│ ├── composer.json
│ ├── composer.lock
│ ├── config
│ ├── route
│ │ └── app.php
│ ├── think
│ ├── vendor
│ └── .env
ThinkPHP
使用 composer 创建 php_openapi 项目,并且安装 predis、php-jwt 扩展包。
[manongsen@root ~]$ pwd
/home/manongsen/workspace/php_to_go/php_openapi
[manongsen@root php_openapi]$ composer create-project topthink/think php_openapi
[manongsen@root php_openapi]$ cp .example.env .env[manongsen@root php_openapi]$ composer require predis/predis
[manongsen@root php_openapi]$ composer require firebase/php-jwt
使用 ThinkPHP 框架提供的命令行工具 php think 创建控制器、中间件、模型文件。
[manongsen@root php_openapi]$ php think make:model Tenant
Model:app\model\Tenant created successfully.[manongsen@root php_openapi]$ php think make:controller Auth
Controller:app\controller\Auth created successfully.[manongsen@root php_openapi]$ php think make:controller User
Controller:app\controller\User created successfully.[manongsen@root php_openapi]$ php think make:middleware ApiAuth
Middleware:app\middleware\ApiAuth created successfully.
在 route/app.php 文件中定义接口的路由。
<?php
use think\facade\Route;Route::post('auth/access', 'auth/accessToken');
Route::post('auth/exchange', 'auth/exchangeToken');
Route::post('auth/refresh', 'auth/refreshToken');// 指定使用 ApiAuth 中间件
Route::group('user', function () {Route::get('info', 'user/info');
})->middleware(\app\middleware\ApiAuth::class);
从下面这个控制器 Auth 文件可以看出有 accessToken()、exchangeToken()、refreshToken() 三个方法,分别对应的都是三个 API 接口。这里会使用 JWT 来生成 Token 令牌,然后统一存储到 Redis 缓存中。其中 accessToken 的有效时间通常会比 refreshToken 长,但在业务接口的实际调用中使用的是 refreshToken。
<?phpnamespace app\controller;use app\BaseController;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use app\model\Tenant;
use think\facade\Cache;
use think\facade\Env;class Auth extends BaseController
{/*** 生成一个 AccessToken*/public function accessToken(){// 获取 AppKey 和 AppSecret 参数$params = $this->request->param();if (!isset($params["app_key"])) {return json(["code" => 400, "msg" => "AppKey参数缺失"]);}$appKey = $params["app_key"];if (empty($appKey)) {return json(["code" => 400, "msg" => "AppKey参数为空"]);}if (!isset($params["app_secret"])) {return json(["code" => 400, "msg" => "AppSecret参数缺失"]);}$appSecret = $params["app_secret"];if (empty($appSecret)) {return json(["code" => 400, "msg" => "AppSecret参数为空"]);}// 在数据库中判断 AppKey 和 AppSecret 是否存在$tenant = Tenant::where('app_key', $appKey)->where('app_secret', $appSecret)->find();if (is_null($tenant)) {return json(["code" => 400, "msg" => "AppKey或AppSecret参数无效"]);}// 生成一个 AccessToken$expiresIn = 7 * 24 * 3600; // 7 天内有效$nowTime = time();$payload = ["iss" => "manongsen", // 签发者 可以为空"aud" => "tenant", // 面向的用户,可以为空"iat" => $nowTime, // 签发时间"nbf" => $nowTime, // 生效时间"exp" => $nowTime + $expiresIn, // AccessToken 过期时间];$accessToken = JWT::encode($payload, $tenant->app_secret, "HS256");$scope = $tenant->scope;$data = ["access_token" => $accessToken, // 访问令牌"token_type" => "bearer", // 令牌类型"expires_in" => $expiresIn, // 过期时间,单位为秒"scope" => $scope, // 权限范围];// 存储到 Redis$redis = Cache::store('redis')->handler();$redis->set(sprintf("%s.%s", Env::get("ACCESS_TOKEN_PREFIX"), $accessToken), $appKey, $expiresIn);return json_encode(["code" => 200, "msg"=>"ok", "data" => $data]);}/*** 通过 AccessToken 换取 RefreshToken*/public function exchangeToken(){// 获取 AccessToken 参数$params = $this->request->param();if (!isset($params["access_token"])) {return json(["code" => 400, "msg" => "AccessToken参数缺失"]);}$accessToken = $params["access_token"];if (empty($accessToken)) {return json(["code" => 400, "msg" => "AccessToken参数为空"]);}// 校验 AccessToken$redis = Cache::store('redis')->handler();$appKey = $redis->get(sprintf("%s.%s", Env::get("ACCESS_TOKEN_PREFIX"), $accessToken));if (empty($appKey)) {return json(["code" => 400, "msg" => "AccessToken参数失效"]);}$tenant = Tenant::where('app_key', $appKey)->find();if (is_null($tenant)) {return json(["code" => 400, "msg" => "AccessToken参数失效"]);}$expiresIn = 2 * 3600; // 2 小时内有效$nowTime = time();$payload = ["iss" => "manongsen", // 签发者, 可以为空"aud" => "tenant", // 面向的用户, 可以为空"iat" => $nowTime, // 签发时间"nbf" => $nowTime, // 生效时间"exp" => $nowTime + $expiresIn, // RefreshToken 过期时间];$refreshToken = JWT::encode($payload, $tenant->app_secret, "HS256");// 颁发 RefreshToken$data = ["refresh_token" => $refreshToken, // 刷新令牌"expires_in" => $expiresIn, // 过期时间,单位为秒];// 存储到 Redis$redis = Cache::store('redis')->handler();$redis->set(sprintf("%s.%s", Env::get("REFRESH_TOKEN_PREFIX"), $refreshToken), $appKey, $expiresIn);return json_encode(["code" => 200, "msg"=>"ok", "data" => $data]);}/*** 刷新 RefreshToken*/public function refreshToken(){// 获取 RefreshToken 参数$params = $this->request->param();if (!isset($params["refresh_token"])) {return json(["code" => 400, "msg" => "RefreshToken参数缺失"]);}$refreshToken = $params["refresh_token"];if (empty($refreshToken)) {return json(["code" => 400, "msg" => "RefreshToken参数为空"]);}// 校验 RefreshToken$redis = Cache::store('redis')->handler();$appKey = $redis->get(sprintf("%s.%s", Env::get("REFRESH_TOKEN_PREFIX"), $refreshToken));if (empty($appKey)) {return json(["code" => 400, "msg" => "RefreshToken参数失效"]);}$tenant = Tenant::where('app_key', $appKey)->find();if (is_null($tenant)) {return json(["code" => 400, "msg" => "RefreshToken参数失效"]);}// 颁发一个新的 RefreshToken$expiresIn = 2 * 3600; // 2 小时内有效$nowTime = time();$payload = ["iss" => "manongsen", // 签发者 可以为空"aud" => "tenant", // 面向的用户,可以为空"iat" => $nowTime, // 签发时间"nbf" => $nowTime, // 生效时间"exp" => $nowTime + $expiresIn, // RefreshToken 过期时间];$newRefreshToken = JWT::encode($payload, $tenant->app_secret, "HS256");$data = ["refresh_token" => $newRefreshToken, // 新的刷新令牌"expires_in" => $expiresIn, // 过期时间,单位为秒];// 将新的 RefreshToken 存储到 Redis$redis = Cache::store('redis')->handler();$redis->set(sprintf("%s.%s", Env::get("REFRESH_TOKEN_PREFIX"), $newRefreshToken), $appKey, $expiresIn);// 删除旧的 RefreshToken$redis->del(sprintf("%s.%s", Env::get("REFRESH_TOKEN_PREFIX"), $refreshToken));return json_encode(["code" => 200, "msg"=>"ok", "data" => $data]);}
}
启动 php_openapi 服务。
[manongsen@root php_openapi]$ php think run
ThinkPHP Development server is started On <http://0.0.0.0:8000/>
You can exit with `CTRL-C`
Document root is: /home/manongsen/workspace/php_to_go/php_openapi/public
[Wed Jul 3 22:02:16 2024] PHP 8.3.4 Development Server (http://0.0.0.0:8000) started
使用 Postman 工具在 Header 上设置 Authorization 参数「即 RefreshToken」便可以成功的返回数据。

Gin
使用 go mod init 初始化 go_openapi 项目,再使用 go get 安装相应的第三方依赖库。
[manongsen@root ~]$ pwd
/home/manongsen/workspace/php_to_go/go_openapi
[manongsen@root go_openapi]$ go mod init go_openapi[manongsen@root go_openapi]$ go get github.com/gin-gonic/gin
[manongsen@root go_openapi]$ go get gorm.io/gorm
[manongsen@root go_openapi]$ go get github.com/golang-jwt/jwt/v4
[manongsen@root go_openapi]$ go get github.com/go-redis/redis
在 Gin 中没有类似 php think 的命令行工具,因此需要自行创建 controller、middleware、model 等文件。
在 app/route.go 路由文件中定义接口,和在 ThinkPHP 中的使用差不多并无两样。
package appimport ("go_openapi/app/controller""go_openapi/app/middleware""github.com/gin-gonic/gin"
)func InitRoutes(r *gin.Engine) {r.POST("/auth/access", controller.AccessToken)r.POST("/auth/exchange", controller.ExchangeToken)r.POST("/auth/refresh", controller.RefreshToken)// 指定使用 ApiAuth 中间件user := r.Group("/user/").Use(middleware.ApiAuth())user.GET("info", controller.UserInfo)
}
同样在 Gin 的控制器中也是三个方法对应三个接口。
package controllerimport ("fmt""go_openapi/app/config""go_openapi/app/model""net/http""time""github.com/gin-gonic/gin""github.com/golang-jwt/jwt"
)// 生成一个 AccessToken
func AccessToken(c *gin.Context) {// 获取 AppKey 和 appSecret 参数appKey := c.PostForm("app_key")if len(appKey) == 0 {c.JSON(http.StatusOK, gin.H{"code": 400,"msg": "AppKey参数为空",})return}appSecret := c.PostForm("app_secret")if len(appSecret) == 0 {c.JSON(http.StatusOK, gin.H{"code": 400,"msg": "appSecret参数为空",})return}// 在数据库中判断 AppKey 和 appSecret 是否存在var tenant *model.TenantdbRes := config.DemoDB.Model(&model.Tenant{}).Where("app_key = ?", appKey).Where("app_secret = ?", appSecret).First(&tenant)if dbRes.Error != nil {c.JSON(http.StatusOK, gin.H{"code": 500,"msg": "内部服务错误",})return}// 生成一个 AccessTokenexpiresIn := int64(7 * 24 * 3600) // 7 天内有效nowTime := time.Now().Unix()jwtToken := jwt.New(jwt.SigningMethodHS256)claims := jwtToken.Claims.(jwt.MapClaims)claims["iss"] = "manongsen" // 签发者 可以为空claims["aud"] = "tenant" // 面向的用户,可以为空claims["iat"] = nowTime // 签发时间claims["nbf"] = nowTime // 生效时间claims["exp"] = nowTime + expiresIn // AccessToken 过期时间accessToken, err := jwtToken.SignedString([]byte(tenant.AppSecret))if err != nil {c.JSON(http.StatusOK, gin.H{"code": 500,"msg": "内部服务错误",})return}scope := tenant.Scopedata := map[string]interface{}{"access_token": accessToken, // 访问令牌"token_type": "bearer", // 令牌类型"expires_in": expiresIn, // 过期时间,单位为秒"scope": scope, // 权限范围}// 存储 AccessToken 到 Redisconfig.RedisConn.Set(fmt.Sprintf("%s.%s", config.ACCESS_TOKEN_PREFIX, accessToken), tenant.AppKey, time.Second*time.Duration(expiresIn)).Result()c.JSON(http.StatusOK, gin.H{"code": 200,"msg": "ok","data": data,})
}// 通过 AccessToken 换取 RefreshToken
func ExchangeToken(c *gin.Context) {// 获取 AccessToken 参数accessToken := c.PostForm("access_token")if len(accessToken) == 0 {c.JSON(http.StatusOK, gin.H{"code": 400,"msg": "AccessToken参数为空",})return}// 校验 AccessTokenappKey, err := config.RedisConn.Get(fmt.Sprintf("%s.%s", config.ACCESS_TOKEN_PREFIX, accessToken)).Result()if err != nil {c.JSON(http.StatusOK, gin.H{"code": 500,"msg": "内部服务错误",})return}if len(appKey) == 0 {c.JSON(http.StatusOK, gin.H{"code": 400,"msg": "AccessToken参数失效",})return}var tenant *model.TenantdbRes := config.DemoDB.Model(&model.Tenant{}).Where("app_key = ?", appKey).First(&tenant)if dbRes.Error != nil {c.JSON(http.StatusOK, gin.H{"code": 500,"msg": "内部服务错误",})return}expiresIn := int64(2 * 3600) // 2 小时内有效nowTime := time.Now().Unix()jwtToken := jwt.New(jwt.SigningMethodHS256)claims := jwtToken.Claims.(jwt.MapClaims)claims["iss"] = "manongsen" // 签发者 可以为空claims["aud"] = "tenant" // 面向的用户,可以为空claims["iat"] = nowTime // 签发时间claims["nbf"] = nowTime // 生效时间claims["exp"] = nowTime + expiresIn // RefreshToken 过期时间refreshToken, err := jwtToken.SignedString([]byte(tenant.AppSecret))if err != nil {c.JSON(http.StatusOK, gin.H{"code": 500,"msg": "内部服务错误",})return}// 颁发 RefreshTokendata := map[string]interface{}{"refresh_token": refreshToken, // 刷新令牌"expires_in": expiresIn, // 过期时间,单位为秒}// 存储到 Redisconfig.RedisConn.Set(fmt.Sprintf("%s.%s", config.REFRESH_TOKEN_PREFIX, refreshToken), appKey, time.Second*time.Duration(expiresIn))c.JSON(http.StatusOK, gin.H{"code": 200,"msg": "ok","data": data,})
}// 刷新 RefreshToken
func RefreshToken(c *gin.Context) {// 获取 RefreshToken 参数refreshToken := c.PostForm("refresh_token")if len(refreshToken) == 0 {c.JSON(http.StatusOK, gin.H{"code": 400,"msg": "RefreshToken参数为空",})return}// 校验 RefreshTokenappKey, err := config.RedisConn.Get(fmt.Sprintf("%s.%s", config.REFRESH_TOKEN_PREFIX, refreshToken)).Result()if err != nil {c.JSON(http.StatusOK, gin.H{"code": 500,"msg": "内部服务错误",})}if len(appKey) == 0 {c.JSON(http.StatusOK, gin.H{"code": 400,"msg": "AccessToken参数失效",})return}var tenant *model.TenantdbRes := config.DemoDB.Model(&model.Tenant{}).Where("app_key = ?", appKey).First(&tenant)if dbRes.Error != nil {c.JSON(http.StatusOK, gin.H{"code": 500,"msg": "内部服务错误",})return}// 颁发一个新的 RefreshTokenexpiresIn := int64(2 * 3600) // 2 小时内有效nowTime := time.Now().Unix()jwtToken := jwt.New(jwt.SigningMethodHS256)claims := jwtToken.Claims.(jwt.MapClaims)claims["iss"] = "manongsen" // 签发者 可以为空claims["aud"] = "tenant" // 面向的用户,可以为空claims["iat"] = nowTime // 签发时间claims["nbf"] = nowTime // 生效时间claims["exp"] = nowTime + expiresIn // RefreshToken 过期时间newRefreshToken, err := jwtToken.SignedString([]byte(tenant.AppSecret))if err != nil {c.JSON(http.StatusOK, gin.H{"code": 500,"msg": "内部服务错误",})return}data := map[string]interface{}{"refresh_token": newRefreshToken, // 新的刷新令牌"expires_in": expiresIn, // 过期时间,单位为秒}// 将新的 RefreshToken 存储到 Redisconfig.RedisConn.Set(fmt.Sprintf("%s.%s", config.REFRESH_TOKEN_PREFIX, newRefreshToken), appKey, time.Second*time.Duration(expiresIn))// 删除旧的 RefreshTokenconfig.RedisConn.Del(fmt.Sprintf("%s.%s", config.REFRESH_TOKEN_PREFIX, refreshToken))c.JSON(http.StatusOK, gin.H{"code": 200,"msg": "ok","data": data,})
}
启动 go_openapi 服务。
[manongsen@root go_openapi]$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.- using env: export GIN_MODE=release- using code: gin.SetMode(gin.ReleaseMode)[GIN-debug] POST /auth/access --> go_openapi/app/controller.AccessToken (3 handlers)
[GIN-debug] POST /auth/exchange --> go_openapi/app/controller.ExchangeToken (3 handlers)
[GIN-debug] POST /auth/refresh --> go_openapi/app/controller.RefreshToken (3 handlers)
[GIN-debug] GET /user/info --> go_openapi/app/controller.UserInfo (4 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8001
使用 Postman 工具在 Header 上设置 Authorization 参数「即 RefreshToken」便可以成功的返回数据。

结语
工作中只要接触过第三方开放平台的都离不开 OpenApi,几乎各大平台都会有自己的 OpenApi 比如微信、淘宝、京东、抖音等。在 OpenApi 对接的过程中最首要的环节就是授权,获取到平台的授权 Token 至关重要。对于我们程序员来说,不仅要能对接 OpenApi 获取到业务数据,还有对其中的授权实现逻辑要有具体的研究,才能通晓其本质做到一通百通。这次我分享的是基于之前公司做 SaaS 平台一些经验的提取,希望能对大家有所帮助。最好的学习就是实践,大家可以手动实践一下,如有需要完整实践代码的朋友可在微信公众号内回复「1087」获取对应的代码。
欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。

相关文章:
PHP转Go系列 | ThinkPHP与Gin框架之OpenApi授权设计实践
大家好,我是码农先森。 我之前待过一个做 ToB 业务的公司,主要是研发以会员为中心的 SaaS 平台,其中涉及的子系统有会员系统、积分系统、营销系统等。在这个 SaaS 平台中有一个重要的角色「租户」,这个租户可以拥有一个或多个子系…...
使用SOAP与TrinityCore交互(待定)
原文:SOAP with TrinityCore | TrinityCore MMo Project Wiki 如何使用SOAP与TC交互 SOAP代表简单对象访问协议,是一种类似于REST的基于标准的web服务访问协议的旧形式。只要必要的配置到位,您就可以利用SOAP向TrinityCore服务器发送命令。 …...
QQ频道导航退出
若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/140413538 长沙红胖子Qt(长沙创微智科)博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV…...
MySQL里的累计求和
在MySQL中,你可以使用SUM()函数来进行累计求和。如果你想要对一个列进行累计求和,可以使用OVER()子句与ORDER BY子句结合,进行窗口函数的操作。 以下是一个简单的例子,假设我们有一个名为sales的表,它有两个列&#x…...
Python爬虫速成之路(3):下载图片
hello hello~ ,这里是绝命Coding——老白~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹 💥个人主页:绝命Coding-CSDN博客 &a…...
同三维T80004EA编解码器视频使用操作说明书:高清HDMI编解码器,高清SDI编解码器,4K超清HDMI编解码器,双路4K超高清编解码器
同三维T80004EA编解码器视频使用操作说明书:高清HDMI编解码器,高清SDI编解码器,4K超清HDMI编解码器,双路4K超高清编解码器 同三维T80004EA编解码器视频使用操作说明书:高清HDMI编解码器,高清SDI编解码器&am…...
ChatGPT提问获取高质量答案的艺术PDF下载书籍推荐分享
ChatGPT高质量prompt技巧分享pdf, ChatGPT提问获取高质量答案的艺术pdf。本书是一本全面的指南,介绍了各种 Prompt 技术的理解和利用,用于从 ChatGPTmiki sharing中生成高质量的答案。我们将探讨如何使用不同的 Prompt 工程技术来实现不同的目…...
微信小程序中的数据通信
方法1: 使用回调函数 在app.js中:可以在修改globalData后执行一个回调函数,这个回调函数可以是页面传递给app的一个更新函数。// app.js App({globalData: {someData: ,},setSomeData(newData, callback) {this.globalData.someData = newData;if (typeof callback === funct…...
everything搜索不到任何文件-设置
版本: V1.4.1.1024 (x64) 问题:搜索不到任何文件 click:[工具]->[选项]->下图所示 将本地磁盘都选中包含...
python如何结束程序运行
方法1:采用sys.exit(0),正常终止程序,从图中可以看到,程序终止后shell运行不受影响。 方法2:采用os._exit(0)关闭整个shell,从图中看到,调用sys._exit(0)后整个shell都重启了(RESTAR…...
InnoDB
InnoDB 是 MySQL 默认的存储引擎,它提供了事务支持、行级锁定和外键约束等高级功能。下面详细解析 InnoDB 的一些底层原理和关键特性。 1. 数据存储结构 表空间(Tablespace) InnoDB 使用表空间来管理数据存储,表空间可以是共享…...
spark运行报错:Container killed by YARN for exceeding memory limits
用spark跑数据量大的离线调度任务报错:Reason: Container killed by YARN for exceeding memory limits. 19.0 GB of 19 GB physical memory used. Consider boosting spark.yarn.executor.memoryOverhead or disabling yarn.nodemanager.vmem-check-enabled becaus…...
(三)大模型/人工智能/机器学习/深度学习/NLP
一.模型 模型,简单来说,就是用来表示或解释某个事物、现象或系统的一种工具或框架。它可以是实体的,也可以是虚拟的,目的是为了帮助我们更好地理解和预测所描述的对象。在生活中,模型无处不在,它们以各种形…...
数学基础 -- 三角学
三角学 三角学(Trigonometry)是数学的一个分支,主要研究三角形的边长与角度之间的关系。三角学在几何学、物理学、工程学等多个领域中有广泛的应用。以下是三角学的一些基本概念和公式: 基本概念 直角三角形:一个角…...
基于BitMap的工作日间隔计算
背景问题 在我们实际开发过程中,时常会遇到日期的间隔计算,即计算多少工作日之后的日期,在不考虑法定节假日的情况下也不是那么复杂,毕竟周六、周日是相对固定的,Java语言也提供了丰富的类来处理此问题。 然而&#x…...
sqlite3 — DB-API 2.0 interface for SQLite databases
sqlite3 — DB-API 2.0 interface for SQLite databases — Python 3.12.4 documentation sqlite3 — DB-API 2.0 interface for SQLite databasessqlite3 — SQLite数据库的DB-API 2.0接口 Source code: Lib/sqlite3/ 源代码位置:Lib/sqlite3/ SQLite is a C…...
Spring Boot中的安全配置与实现
Spring Boot中的安全配置与实现 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将深入探讨Spring Boot中的安全配置与实现,看看如何保护你的…...
DepthAnything(2): 基于ONNXRuntime在ARM(aarch64)平台部署DepthAnything
DepthAnything(1): 先跑一跑Depth Anything_depth anything离线怎么跑-CSDN博客 目录 1. 写在前面 2. 安装推理组件 3. 生成ONNX 4. 准备ONNXRuntime库 5. API介绍 6. 例程 1. 写在前面 DepthAnything是一种能在任何情况下处理任何图像的简单却又强大的深度估计模型。 …...
JAVA简单封装UserUtil
目录 思路 一、TokenFilterConfiguration 二、FilterConfig 三、TokenContextHolder 四、TokenUtil 五、UserUtil 思路 配置Token过滤器(TokenFilterConfiguration):实现一个Token过滤器配置,用于拦截HTTP请求,从请求头中提取Token&…...
【TOOLS】Chrome扩展开发
Chrome Extension Development 1. 入门教程 入门案例,可以访问【 谷歌插件官网官方文档 】查看官方入门教程,这里主要讲解大概步骤 Chrome Extenson 没有固定的脚手架,所以项目的搭建需要根据开发者自己根据需求搭建项目(例如通过…...
2025届毕业生推荐的降重复率平台实际效果
Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 针对维普检测系统具备能识别 AI 生成内容的特性情形之下,若要降低文本里的 AI 痕…...
使用 winget 卸载 SQLiteStudio:从命令到细节的完整指南
一条命令安装,一条命令卸载——winget 让 Windows 软件管理变得前所未有的简单 前言 SQLiteStudio 是一款轻量、跨平台的 SQLite 数据库管理工具,因其简洁的界面和强大的功能,深受开发者喜爱。在 Windows 上,越来越多的人选择通过微软官方包管理器 winget 来安装它: win…...
simulink和carsim联合仿真的mpc轨迹跟踪模型。
simulink和carsim联合仿真的mpc轨迹跟踪模型。MPC(模型预测控制)轨迹跟踪模型在Simulink和Carsim联合仿真中,通过构建车辆动力学模型、设计MPC控制器,实现对车辆轨迹的精准跟踪。其代码涉及车辆状态方程、MPC优化算法等核心部分。…...
AO3镜像站技术架构与部署指南:构建高可用同人作品访问平台
AO3镜像站技术架构与部署指南:构建高可用同人作品访问平台 【免费下载链接】AO3-Mirror-Site 项目地址: https://gitcode.com/gh_mirrors/ao/AO3-Mirror-Site Archive of Our Own(AO3)作为全球最大的非营利性同人作品平台,…...
PyCharm与Git高效协作:从配置到团队开发的完整指南
1. PyCharm与Git的黄金组合:为什么它们是天作之合 第一次接触PyCharm和Git的组合时,我还在用传统的FTP上传代码。直到某次误删了重要文件,才意识到版本控制的重要性。现在每次看到新手还在手动备份代码文件夹,我都想冲上去安利这…...
佣金自动算、订单自动记,这才叫好系统
做推客、做分销、做私域小店,最磨人的从来不是拉新和卖货,而是没完没了的记账、对账、算佣金。人工统计订单、Excel 算佣金、靠截图核对业绩,不仅慢、容易错,还特别消耗信任。真正能让商家省心、让推客放心的好系统,标…...
工厂升级不换设备?揭秘全志T113-i边缘网关的“万能翻译”魔法
在当今智能制造和工业物联网的浪潮下,工厂车间正经历着一场深刻的“神经”系统升级。以PROFINET、EtherNet/IP、Modbus TCP为代表的工业以太网协议,凭借其高速、实时、开放的特性,已成为现代自动化系统的“中枢神经”。然而,走进许…...
告别重复造轮子:用快马生成高效配对模块提升开发效率
在开发智能硬件或物联网项目时,设备配对功能几乎是每个项目都绕不开的基础模块。但每次从零开始实现蓝牙、Wi-Fi等设备的配对逻辑时,总免不了要重复处理扫描过滤、状态管理、错误重试这些"轮子"。最近尝试用InsCode(快马)平台生成标准化配对模…...
敏捷测试实践:两周一个迭代的质量保障
在软件快速交付的时代,以两周为一个迭代周期的敏捷开发模式已成为行业主流。对于测试从业者而言,这既是挑战也是机遇。传统的“瀑布式”测试在漫长的周期后介入的模式已彻底失效,质量保障活动必须无缝融入高速运转的迭代流水线,从…...
MATLAB报错解析:深入理解eval与struct类型冲突的根源及修复方法
1. 从报错现象看MATLAB底层机制 第一次遇到"错误使用eval,未定义与struct类型的输入参数相对应的函数workspacefunc"这个报错时,我盯着红色报错信息愣了半天。作为用了MATLAB七八年的老用户,这种底层函数报错还真不多见。后来在论坛…...
