This commit is contained in:
杨丹华
2025-05-22 16:20:32 +08:00
33 changed files with 1315 additions and 4 deletions

View File

@@ -69,7 +69,7 @@
<ul class="love2"> <ul class="love2">
{volist name="recommends" id="vo"} {volist name="recommends" id="vo"}
<li> <li>
<a> <a href="{:url('article/detail', ['id' => $vo.id])}">
<div class="lvimg"><img src="{$vo.image}"></div> <div class="lvimg"><img src="{$vo.image}"></div>
<p class="lvtit">{$vo.title}</p> <p class="lvtit">{$vo.title}</p>
</a> </a>

View File

@@ -19,8 +19,7 @@
</div> </div>
<!-- 赛博云 --> <!-- 赛博云 -->
{notempty name="data.cyber"} {notempty name="data.cyber"}
<!-- narssben-us 这个样式如果是中文就去掉 如果是因为就加在narssbshow后面--> <div class="nDtopCtMian narssbshow {eq name='Request.cookie.think_lang' value='en-us'}narssben-us{/eq}">
<div class="nDtopCtMian narssbshow narssben-us">
{notempty name="data.cyber.focus_image"} {notempty name="data.cyber.focus_image"}
<div class="nDtopIt"> <div class="nDtopIt">
<img src="{$data.cyber.focus_image.image}" class="tpimg" /> <img src="{$data.cyber.focus_image.image}" class="tpimg" />

70
app/openapi/common.php Normal file
View File

@@ -0,0 +1,70 @@
<?php
// 这是系统自动生成的公共文件
if (!function_exists('image_domain_concat')) {
/**
* 图片域名拼接
* @param $path
* @return string
*/
function image_domain_concat($path)
{
if (empty($path)) {
return '';
}
$domain = env('OPENAPI.RESOURCE_IMAGES_DOMAIN');
if (empty($domain)) {
return $path;
}
return rtrim($domain, '/') . '/' . ltrim($path, '/');
}
}
if (!function_exists('video_domain_concat')) {
/**
* 视频域名拼接
* @param $path
* @return string
*/
function video_domain_concat($path)
{
if (empty($path)) {
return '';
}
$domain = env('OPENAPI.RESOURCE_VIDEOS_DOMAIN');
if (empty($domain)) {
return $path;
}
return rtrim($domain, '/') . '/' . ltrim($path, '/');
}
}
if (!function_exists('html_image_replace')) {
/**
* 替换html中的图片路径
* @param $html
* @return string
*/
function html_image_replace($html)
{
if (empty($html)) {
return '';
}
return preg_replace_callback('/<img[^>]+src\s*=\s*([\'"])((?:(?!\1).)*)\1[^>]*>/i',
function($matches) {
$src = $matches[2];
if (!empty($src) && !str_starts_with($src, 'http')) {
// 保留原始标签只替换src属性
return str_replace($src, image_domain_concat($src), $matches[0]);
}
return $matches[0];
},
$html
);
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare (strict_types = 1);
namespace app\openapi\controller\v1;
use app\openapi\model\ArticleModel;
class Article
{
/**
* 文章列表
*/
public function list()
{
$params = request()->get([
'category_id',
'language' => 'zh-cn',
'page/d' => 1,
'size/d' => 50
]);
if ($params['size'] > 200) {
// 每页不超过200条
$params['size'] = 200;
}
$articles = ArticleModel::with([
'category' => fn($query) => $query->field('id, name')
])
->field([
'id',
'language_id',
'category_id',
'title',
'desc',
'image',
'deleted_at'
])
->categoryId($params['category_id']??null)
->language($params['language']??'zh-cn')
->hidden(['language_id', 'category_id'])
->paginate([
'list_rows' => $params['size'],
'page' => $params['page']
])
->each(function($it) {
if (!empty($it['image']) && !str_starts_with($it['image'], 'http')) {
$it['image'] = image_domain_concat($it['image']);
}
return $it;
});
return success('success', $articles);
}
/**
* 文章详情
*/
public function detail()
{
$id = request()->param('id');
$article = ArticleModel::with([
'category' => fn($query) => $query->field('id, name')
])
->withoutField([
'language_id',
'seo_title',
'seo_keywords',
'seo_desc',
'created_at',
'updated_at',
'deleted_at'
])
->bypk($id)
->hidden(['category_id'])
->find();
if (is_null($article)) {
return error('the article does not exist');
}
// 图片处理
if (!empty($article['image']) && !str_starts_with($article['image'], 'http')) {
$article['image'] = image_domain_concat($article['image']);
}
// 详情中图片处理
if (!empty($article['content'])) {
$article['content'] = html_image_replace($article['content']);
}
return success('success', $article);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare (strict_types = 1);
namespace app\openapi\controller\v1;
use app\openapi\model\ArticleCategoryModel;
class ArticleCategory
{
/**
* 文章分类列表
*/
public function list()
{
$params = request()->get([
'parent_id',
'language' => 'zh-cn',
'page/d' => 1,
'size/d' => 50
]);
if ($params['size'] > 200) {
// 每页不超过200条
$params['size'] = 200;
}
$categories = ArticleCategoryModel::withoutField([
'language_id',
'unique_label',
'created_at',
'updated_at',
])
->language($params['language']??'zh-cn')
->parent($params['parent_id']??null)
->order(['sort' => 'asc', 'id' => 'desc'])
->paginate([
'list_rows' => $params['size'],
'page' => $params['page'],
])
->each(function($item) {
if (!empty($item['icon']) && !str_starts_with($item['icon'], 'http')){
$item['icon'] = image_domain_concat($item['icon']);
}
return $item;
});
return success('success', $categories);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare (strict_types = 1);
namespace app\openapi\controller\v1;
use OAuth2\OAuth2;
use OAuth2\OAuth2ServerException;
use oauth\OAuthStorage;
use Symfony\Component\HttpFoundation\Request;
class Authorize
{
/**
* 获取/刷新token
*/
public function token()
{
try {
$post = request()->post([
'client_id',
'client_secret',
'grant_type',
'refresh_token',
]);
$server = request()->server();
$request = new Request([], $post, [], [], [], $server);
$storage = new OAuthStorage;
$oauth = new OAuth2($storage);
$token = $oauth->grantAccessToken($request);
return success('success', json_decode($token->getContent(), true));
} catch (OAuth2ServerException $e) {
return error($e->getMessage() . ' - ' . $e->getDescription());
} catch (\Throwable $th) {
return error($th->getMessage());
}
}
}

View File

@@ -0,0 +1,174 @@
<?php
declare (strict_types = 1);
namespace app\openapi\controller\v1;
use app\openapi\model\ProductModel;
class Product
{
/**
* 产品列表
*/
public function list()
{
$params = request()->get([
'category_id',
'language' => 'zh-cn',
'page/d' => 1,
'size/d' => 50
]);
if ($params['size'] > 200) {
// 每页不超过200条
$params['size'] = 200;
}
$list = ProductModel::with([
'category' => fn($query) => $query->field(['id', 'name']),
])
->field([
'id',
'category_id',
'spu',
'name',
'short_name',
'cover_image',
'desc',
'deleted_at'
])
->where(function($query) use($params) {
$model = $query->getModel();
if (!empty($params['category_id'])) {
$model->scopeCategoryId($query, $params['category_id']);
}
})
->language($params['language']??'zh-cn')
->order(['sort' => 'asc', 'id' => 'desc'])
->hidden(['category_id'])
->paginate([
'list_row' => $params['size'],
'page' => $params['page']
])
->each(function($item) {
if (!empty($item['cover_image']) && !str_starts_with($item['cover_image'], 'http')) {
$item['cover_image'] = image_domain_concat($item['cover_image']);
}
return $item;
});
return success('success', $list);
}
/**
* 产品详情
*/
public function detail()
{
$id = request()->param('id');
$product = ProductModel::with([
// 关联分类
'category' => fn($query) => $query->field(['id', 'name']),
// 关联属性
'params' => fn($query) => $query->field(['product_id', 'name', 'value'])
->hidden(['product_id']),
// 关联sku
'skus' => fn($query) => $query->withoutField(['created_at', 'updated_at'])
->with([
'sku_attr' => fn($query) => $query->with('attr')->hidden(['sku_id', 'attr_id'])
])
->hidden(['id', 'product_id']),
// 关联购买链接
'links' => fn($query) => $query->field(['product_id', 'platform_id', 'link'])
->with(['platform' => fn($query) => $query->field(['id', 'platform'])])
->hidden(['product_id', 'platform_id']),
// 关联相关产品
'related' => fn($query) => $query->field(['product_id', 'related_product_id'])
->with([
'product' => fn($query) => $query->field(['id', 'name', 'spu', 'cover_image'])->withBind([
'id',
'name',
'spu',
'cover_image'
])
])
->hidden(['product_id', 'related_product_id'])
])
->withoutField([
'language_id',
'stock_qty',
'seo_title',
'seo_keywords',
'seo_desc',
'created_at',
'updated_at',
'deleted_at'
])
->bypk($id)
->hidden(['category_id'])
->find();
// 处理封面图
if (!empty($product['cover_image']) && !str_starts_with($product['cover_image'], 'http')) {
$product['cover_image'] = image_domain_concat($product['cover_image']);
}
// 处理视频图片
if (!empty($product['video_img']) && !str_starts_with($product['video_img'], 'http')) {
$product['video_img'] = image_domain_concat($product['video_img']);
}
// 处理视频
if (!empty($product['video_url']) && !str_starts_with($product['video_url'], 'http')) {
$product['video_url'] = video_domain_concat($product['video_url']);
}
// 处理详情内容中图片
if (!empty($product['detail'])) {
$product['detail'] = html_image_replace($product['detail']);
}
// 处理sku中图片
if (!empty($product['skus'])) {
$skus = $product['skus']->toArray();
foreach ($skus as $key => $sku) {
// sku二级列表图
if (!empty($sku['main_image']) && !str_starts_with($sku['main_image'], 'http')) {
$skus[$key]['main_image'] = image_domain_concat($sku['main_image']);
}
// sku相册图
if (!empty($sku['photo_album'])) {
$photo_album = json_decode($sku['photo_album'], true);
foreach ($photo_album as $idx => $photo) {
if (!empty($photo) && !str_starts_with($photo, 'http')) {
$photo_album[$idx] = image_domain_concat($photo);
}
}
$skus[$key]['photo_album'] = $photo_album;
}
// sku属性图片
foreach ($sku['sku_attr'] as $idx => $attr) {
if (!empty($attr['attr_value']) && !str_starts_with($attr['attr_value'], 'http')) {
$skus[$key]['sku_attr'][$idx]['attr_value'] = image_domain_concat($attr['attr_value']);
}
}
}
$product['skus'] = $skus;
}
// 处理相关产品中图片
if (!empty($product['related'])) {
$related = $product['related']->toArray();
foreach ($related as $key => $item) {
if (!empty($item['cover_image']) && !str_starts_with($item['cover_image'], 'http')) {
$related[$key]['cover_image'] = image_domain_concat($item['cover_image']);
}
}
$product['related'] = $related;
}
return success('success', $product);
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare (strict_types = 1);
namespace app\openapi\controller\v1;
use app\openapi\model\ProductCategoryModel;
class ProductCategory
{
/**
* 产品分类列表
*/
public function list()
{
$params = request()->get([
'parent_id',
'language' => 'zh-cn',
'page' => 1,
'size' => 50
]);
if ($params['size'] > 200) {
// 每页不超过200条
$params['size'] = 200;
}
$categories = ProductCategoryModel::withoutField([
'language_id',
'unique_id',
'related_tco_category',
'created_at',
'updated_at'
])
->language($params['language']??'zh-cn')
->parent($params['parent_id']??null)
->paginate([
'list_rows' => $params['size'],
'page' => $params['page']
])
->each(function($item) {
if (!empty($item['icon']) && !str_starts_with($item['icon'], 'http')) {
$item['icon'] = image_domain_concat($item['icon']);
}
return $item;
});
return success('success', $categories);
}
}

5
app/openapi/event.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
// 这是系统自动生成的event定义文件
return [
];

View File

@@ -0,0 +1,5 @@
<?php
// 这是系统自动生成的middleware定义文件
return [
];

View File

@@ -0,0 +1,31 @@
<?php
declare (strict_types = 1);
namespace app\openapi\middleware;
use OAuth2\OAuth2;
use OAuth2\OAuth2ServerException;
use oauth\OAuthStorage;
class Auth
{
/**
* 处理请求
*
* @param \think\Request $request
* @param \Closure $next
* @return Response
*/
public function handle($request, \Closure $next)
{
try {
$oauth = new OAuth2(new OAuthStorage);
$token = $oauth->getBearerToken();
$oauth->verifyAccessToken($token);
} catch (OAuth2ServerException $e) {
return error('Unauthorized', $e->sendHttpResponse(), 401);
}
return $next($request);
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use app\common\model\ArticleCategoryBaseModel;
use think\facade\Db;
/**
* 文章分类模型
* @mixin \think\Model
*/
class ArticleCategoryModel extends ArticleCategoryBaseModel
{
// 所属语言范围查询
public function scopeLanguage($query, $language = 'zh-cn')
{
$query->whereExists(function($subquery) use($language) {
$lang_model = new LanguageModel;
$subquery->model($lang_model)
->name($lang_model->getName())
->field(['id'])
->where('id', '=', Db::raw($this->getTable() . '.language_id'))
->where('code', '=', $language);
});
}
// 所属上级分类范围查询
public function scopeParent($query, $parent_id)
{
if (is_null($parent_id)) return;
$query->where('pid', '=', $parent_id);
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use app\common\model\ArticleBaseModel;
use think\facade\Db;
/**
* 文章模型
* @mixin \think\Model
*/
class ArticleModel extends ArticleBaseModel
{
// 关联分类
public function category()
{
return $this->belongsTo(ArticleCategoryModel::class, 'category_id', 'id');
}
// 所属语言范围查询
public function scopeLanguage($query, $language = 'zh-cn')
{
$query->whereExists(function($subquery) use($language) {
$lang_model = new LanguageModel;
$subquery->model($lang_model)
->name($lang_model->getName())
->field(['id'])
->where('id', '=', Db::raw($this->getTable() . '.language_id'))
->where('code', '=', $language);
});
}
// 所属分类范围查询
public function scopeCategoryId($query, $category_id)
{
if (is_null($category_id)) return;
$query->where('category_id', '=', $category_id);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use app\common\model\LanguageBaseModel;
/**
* 语言模型
* @mixin \think\Model
*/
class LanguageModel extends LanguageBaseModel
{
// 所属code范围查询
public function scopeCode($query, $code)
{
$query->where('code', '=', $code);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use think\Model;
/**
* @mixin \think\Model
*/
class OAuthAccessTokenModel extends Model
{
// 表名
protected $name = 'oauth_access_token';
// 字段信息
protected $schema = [
'id' => 'int',
'client_id' => 'string',
'user_id' => 'int',
'access_token' => 'string',
'expires' => 'int',
'scope' => 'string',
'created_at' => 'datetime',
];
// access_token范围查询
public function scopeAccessToken($query, $access_token)
{
$query->where('access_token', '=', $access_token);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use think\Model;
/**
* @mixin \think\Model
*/
class OAuthAuthCodeModel extends Model
{
// 表名
protected $name = 'oauth_auth_code';
// 字段信息
protected $schema = [
'id' => 'int',
'code' => 'string',
'client_id' => 'string',
'user_id' => 'int',
'expires' => 'int',
'redirect_uri' => 'string',
'scope' => 'string',
'created_at' => 'datetime',
];
// code范围查询
public function scopeCode($query, $code)
{
$query->where('code', '=', $code);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use think\Model;
/**
* @mixin \think\Model
*/
class OAuthClientModel extends Model
{
// 表名
protected $name = 'oauth_client';
// 字段信息
protected $schema = [
'id' => 'int',
'client_id' => 'string',
'client_secret' => 'string',
'redirect_uri' => 'string',
'enabled' => 'int',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'deleted_at' => 'datetime'
];
// client_id范围查询
public function scopeClientId($query, $client_id)
{
$query->where('client_id', '=', $client_id);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use think\Model;
/**
* @mixin \think\Model
*/
class OAuthRefreshTokenModel extends Model
{
// 表名
protected $name = 'oauth_refresh_token';
// 字段信息
protected $schema = [
'id' => 'int',
'client_id' => 'string',
'user_id' => 'int',
'refresh_token' => 'string',
'expires' => 'int',
'scope' => 'string',
'created_at' => 'datetime'
];
// refresh_token范围查询
public function scopeRefreshToken($query, $refresh_token)
{
$query->where('refresh_token', '=', $refresh_token);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use app\common\model\ProductCategoryBaseModel;
use think\facade\Db;
/**
* 产品分类模型
* @mixin \think\Model
*/
class ProductCategoryModel extends ProductCategoryBaseModel
{
// 所属语言
public function scopeLanguage($query, $language)
{
$query->whereExists(function($subquery) use($language) {
$lang_model = new LanguageModel;
$subquery->model($lang_model)
->name($lang_model->getName())
->where('id', '=', Db::Raw($this->getTable() . '.language_id'))
->where('code', '=', $language);
});
}
// 所属上级范围查询
public function scopeParent($query, $parent_id)
{
if (is_null($parent_id)) return;
$query->where('pid', '=', $parent_id);
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use app\common\model\ProductBaseModel;
use think\facade\Db;
/**
* 产品模型
* @mixin \think\Model
*/
class ProductModel extends ProductBaseModel
{
// 关联分类
public function category()
{
return $this->belongsTo(ProductCategoryModel::class, 'category_id', 'id');
}
// 关联产品参数
public function params()
{
return $this->hasMany(ProductParamsModel::class, 'product_id', 'id');
}
// 关联sku
public function skus()
{
return $this->hasMany(ProductSkuModel::class, 'product_id', 'id');
}
// 关联购买链接
public function links()
{
return $this->hasMany(ProductPurchaseLinkModel::class, 'product_id', 'id');
}
// 关联相关产品
public function related()
{
return $this->hasMany(ProductRelatedModel::class, 'product_id', 'id');
}
// 根据所属语言范围查询
public function scopeLanguage($query, $language)
{
$query->whereExists(function($subquery) use($language) {
$lang_model = new LanguageModel;
$subquery->model($lang_model)
->name($lang_model->getName())
->where('id', '=', Db::Raw($this->getTable() . '.language_id'))
->where('code', '=', $language);
});
}
// 根据category_id范围查询
public function scopeCategoryId($query, $category_id)
{
$query->where('category_id', '=', $category_id);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use app\common\model\ProductParamsBaseModel;
/**
* 产品参数模型
* @mixin \think\Model
*/
class ProductParamsModel extends ProductParamsBaseModel
{
//
}

View File

@@ -0,0 +1,19 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use app\common\model\ProductPurchaseLinkBaseModel;
/**
* 产品购买链接模型
* @mixin \think\Model
*/
class ProductPurchaseLinkModel extends ProductPurchaseLinkBaseModel
{
// 关联购买平台
public function platform()
{
return $this->belongsTo(ProductPurchasePlatformModel::class, 'platform_id', 'id')->bind(['platform']);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use app\common\model\ProductPurchasePlatformBaseModel;
/**
* 产品购买链接平台模型
* @mixin \think\Model
*/
class ProductPurchasePlatformModel extends ProductPurchasePlatformBaseModel
{
//
}

View File

@@ -0,0 +1,19 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use app\common\model\ProductRelatedBaseModel;
/**
* 产品 - 相关产品模型
* @mixin \think\Model
*/
class ProductRelatedModel extends ProductRelatedBaseModel
{
// 关联产品
public function product()
{
return $this->belongsTo(ProductModel::class, 'related_product_id', 'id');
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use app\common\model\ProductSkuAttrBaseModel;
/**
* 产品sku属性模型
* @mixin \think\Model
*/
class ProductSkuAttrModel extends ProductSkuAttrBaseModel
{
// 关联属性
public function attr()
{
return $this->belongsTo(\app\common\model\ProductAttrBaseModel::class, 'attr_id', 'id')->bind(['attr_name']);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare (strict_types = 1);
namespace app\openapi\model;
use app\common\model\ProductSkuBaseModel;
/**
* 产品sku模型
* @mixin \think\Model
*/
class ProductSkuModel extends ProductSkuBaseModel
{
// 关联属性
public function skuAttr()
{
return $this->hasMany(ProductSkuAttrModel::class, 'sku_id', 'id');
}
}

39
app/openapi/route/v1.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
use think\facade\Route;
Route::group('v1', function() {
// 接口授权
Route::post('authorize', 'v1.Authorize/token');
// 接口授权后才能访问
Route::group(function(){
// 获取产品列表
Route::get('products', 'v1.Product/list');
Route::group('product', function() {
// 获取产品信息
Route::get(':id', 'v1.Product/detail')->when('id', 'number');
// 获取产品分类
Route::get('categories', 'v1.ProductCategory/list');
});
// 获取文章动态
Route::get('articles', 'v1.Article/list');
Route::group('article', function() {
// 获取文章详情
Route::get(':id', 'v1.Article/detail')->when('id', 'number');
// 获取文章分类
Route::get('categories', 'v1.ArticleCategory/list');
});
})
->middleware(\app\openapi\middleware\Auth::class);
})
->middleware(\think\middleware\Throttle::class, [
'visit_rate' => '5/m',
'visit_fail_response' => function (\think\middleware\Throttle $throttle, \think\Request $request, int $wait_seconds) {
return \think\Response::create('您的操作过于频繁, 请在 ' . $wait_seconds . ' 秒后再试。')->code(429);
},
])
->completeMatch(true);

View File

@@ -32,7 +32,8 @@
"topthink/think-throttle": "^2.0", "topthink/think-throttle": "^2.0",
"intervention/image": "^3.10", "intervention/image": "^3.10",
"topthink/think-cors": "^1.0", "topthink/think-cors": "^1.0",
"phpoffice/phpspreadsheet": "^3.8" "phpoffice/phpspreadsheet": "^3.8",
"friendsofsymfony/oauth2-php": "^1.3"
}, },
"require-dev": { "require-dev": {
"symfony/var-dumper": ">=4.2", "symfony/var-dumper": ">=4.2",

View File

@@ -0,0 +1,42 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateOauthClient extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('oauth_client', ['engine' => 'MyISAM', 'collation' => 'utf8mb4_general_ci', 'comment' => 'OAuth 客户端表']);
$table->addColumn('agent_id', 'integer', ['limit' => 10, 'null' => true, 'comment' => '代理ID'])
->addColumn('client_id', 'string', ['limit' => 32, 'comment' => '客户端id'])
->addColumn('client_secret', 'string', ['limit' => 128, 'comment' => '客户端密钥'])
->addColumn('redirect_uri', 'string', ['limit' => 255, 'comment' => '回调地址'])
->addColumn('enabled', 'boolean', ['default' => 1, 'comment' => '是否启用'])
->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间'])
->addColumn('updated_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 'comment' => '更新时间'])
->addColumn('deleted_at', 'timestamp', ['null' => true, 'default' => null, 'comment' => '删除时间'])
->create();
}
}

View File

@@ -0,0 +1,41 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateOauthAuthCode extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('oauth_auth_code', ['engine' => 'MyISAM', 'collation' => 'utf8mb4_general_ci', 'comment' => '授权码表']);
$table->addColumn('code', 'string', ['limit' => 64, 'null' => false, 'default' => '', 'comment' => '授权码'])
->addColumn('client_id', 'integer', ['limit' => 11, 'null' => false, 'default' => 0, 'comment' => '客户端ID'])
->addColumn('user_id', 'integer', ['limit' => 11, 'null' => false, 'default' => 0, 'comment' => '用户ID'])
->addColumn('expires', 'integer', ['null' => true, 'comment' => '过期时间'])
->addColumn('redirect_uri', 'string', ['limit' => 255, 'null' => false, 'default' => '', 'comment' => '回调地址'])
->addColumn('scope', 'string', ['limit' => 255, 'null' => false, 'default' => '', 'comment' => '授权范围'])
->addColumn('created_at', 'timestamp', ['null' => false, 'default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间'])
->create();
}
}

View File

@@ -0,0 +1,40 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateOauthAccessToken extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('oauth_access_token', ['engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => 'OAuth2访问令牌表']);
$table->addColumn('client_id', 'string', ['limit' => 32, 'default' => '', 'comment' => '客户端ID'])
->addColumn('user_id', 'integer', ['limit' => 10, 'default' => 0, 'comment' => '用户ID'])
->addColumn('access_token', 'string', ['limit' => 64, 'default' => '', 'comment' => '访问令牌'])
->addColumn('expires', 'integer', ['limit' => 11, 'default' => 0, 'comment' => '过期时间'])
->addColumn('scope', 'string', ['limit' => 2000, 'default' => '', 'comment' => '授权范围'])
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间'])
->create();
}
}

View File

@@ -0,0 +1,40 @@
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateOauthRefreshToken extends Migrator
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* http://docs.phinx.org/en/latest/migrations.html#the-abstractmigration-class
*
* The following commands can be used in this method and Phinx will
* automatically reverse them when rolling back:
*
* createTable
* renameTable
* addColumn
* renameColumn
* addIndex
* addForeignKey
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('oauth_refresh_token', ['engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => 'OAuth2刷新令牌表']);
$table->addColumn('client_id', 'string', ['limit' => 32, 'default' => '', 'comment' => '客户端ID'])
->addColumn('user_id', 'integer', ['limit' => 10, 'default' => 0, 'comment' => '用户ID'])
->addColumn('refresh_token', 'string', ['limit' => 64, 'default' => '', 'comment' => '刷新令牌'])
->addColumn('expires', 'integer', ['limit' => 11, 'default' => 0, 'comment' => '过期时间'])
->addColumn('scope', 'string', ['limit' => 2000, 'default' => '', 'comment' => '授权范围'])
->addColumn('created_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'comment' => '创建时间'])
->create();
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace oauth;
use app\openapi\model\OAuthAccessTokenModel;
use app\openapi\model\OAuthAuthCodeModel;
use app\openapi\model\OAuthClientModel;
use app\openapi\model\OAuthRefreshTokenModel;
use OAuth2\IOAuth2GrantClient;
use OAuth2\IOAuth2GrantCode;
use OAuth2\IOAuth2RefreshTokens;
use OAuth2\Model\IOAuth2AccessToken;
use OAuth2\Model\IOAuth2AuthCode;
use OAuth2\Model\IOAuth2Client;
use OAuth2\Model\IOAuth2Token;
use OAuth2\Model\OAuth2AccessToken;
use OAuth2\Model\OAuth2AuthCode;
use OAuth2\Model\OAuth2Client;
use OAuth2\Model\OAuth2Token;
/**
* OAuth2存储实现
*/
class OAuthStorage implements IOAuth2GrantCode, IOAuth2RefreshTokens, IOAuth2GrantClient
{
private $salt = '';
public function __construct($salt = null)
{
if (!is_null($salt)) {
$this->salt = $salt;
}
}
public function addClient($client_id, $client_secret, $redirect_uri)
{
// 实现添加客户端的逻辑
$client = OAuthClientModel::create([
'client_id' => $client_id,
'client_secret' => hash('sha1', $client_id . $client_secret . $this->salt),
'redirect_uri' => $redirect_uri,
]);
return !$client->isEmpty();
}
public function getAuthCode($code): IOAuth2AuthCode
{
// 实现获取授权码的逻辑
$ret = OAuthAuthCodeModel::code($code)->find();
if (is_null($ret)) {
throw new \Exception('授权码不存在');
}
return new OAuth2AuthCode($ret->client_id, '', $ret->expires, $ret->scope, null, $ret->redirect_uri);
}
public function createAuthCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null)
{
// 实现创建授权码的逻辑
$auth = OAuthAuthCodeModel::create([
'code' => $code,
'client_id' => $client_id,
'user_id' => $user_id,
'redirect_uri' => $redirect_uri,
'expires' => $expires,
'scope' => $scope,
]);
return !$auth->isEmpty();
}
public function markAuthCodeAsUsed($code)
{
// 实现标记授权码为已使用的逻辑
OAuthAuthCodeModel::code($code)->delete();
}
public function getClient($client_id): IOAuth2Client
{
// 实现获取客户端的逻辑
$ret = OAuthClientModel::clientId($client_id)->find();
if (is_null($ret)) {
throw new \Exception('客户端不存在');
}
if ($ret->enabled != 1) {
throw new \Exception('客户端已禁用');
}
return new OAuth2Client($ret->client_id, $ret->client_secret, [$ret->redirect_uri]);
}
public function checkClientCredentials(IOAuth2Client $client, $client_secret = null): bool
{
// 实现检查客户端凭证的逻辑
$client = OAuthClientModel::clientId($client->getPublicId())->find();
if (is_null($client)) {
return false;
}
return $client->client_secret == hash('sha1', $client->client_id . $client_secret . $this->salt);
}
public function getAccessToken($access_token): IOAuth2AccessToken
{
// 实现获取访问令牌的逻辑
$ret = OAuthAccessTokenModel::accessToken($access_token)->find();
if (is_null($ret)) {
throw new \Exception('访问令牌不存在');
}
return new OAuth2AccessToken($ret->client_id, $ret->access_token, $ret->expires, $ret->scope, null);
}
public function createAccessToken($access_token, IOAuth2Client $client, $user_id, $expires, $scope = null)
{
// 实现创建访问令牌的逻辑
OAuthAccessTokenModel::create([
'access_token' => $access_token,
'client_id' => $client->getPublicId(),
'user_id' => $user_id,
'expires' => $expires,
'scope' => $scope,
]);
}
public function checkClientCredentialsGrant(IOAuth2Client $client, $client_secret): array
{
// 实现检查受限授权类型的逻辑
return ['issue_refresh_token' => true];
}
public function checkRestrictedGrantType(IOAuth2Client $client, $grant_type): bool
{
// 实现检查受限授权类型的逻辑
return $grant_type == 'client_credentials' || $grant_type == 'refresh_token';
}
// IOAuth2RefreshTokens 接口方法实现
public function getRefreshToken($refresh_token): IOAuth2Token
{
// 实现获取刷新令牌的逻辑
$ret = OAuthRefreshTokenModel::refreshToken($refresh_token)->find();
if (is_null($ret)) {
throw new \Exception('刷新令牌不存在');
}
return new OAuth2Token($ret->client_id, $ret->refresh_token, $ret->expires, $ret->scope, null);
}
public function createRefreshToken($refresh_token, IOAuth2Client $client, $user_id, $expires, $scope = null)
{
// 实现创建刷新令牌的逻辑
OAuthRefreshTokenModel::create([
'refresh_token' => $refresh_token,
'client_id' => $client->getPublicId(),
'user_id' => $user_id,
'expires' => $expires,
'scope' => $scope,
]);
}
public function unsetRefreshToken($refresh_token)
{
// 实现注销刷新令牌的逻辑
OAuthRefreshTokenModel::refreshToken($refresh_token)->delete();
}
}