From 4828bde6957f660b245c9b94906e9b9b4e122bff Mon Sep 17 00:00:00 2001 From: jsasg <735273025@qq.com> Date: Tue, 20 May 2025 14:40:39 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E5=BC=80=E6=94=BEAPI=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/openapi/common.php | 2 + app/openapi/controller/v1/Authorize.php | 37 ++++ app/openapi/event.php | 5 + app/openapi/middleware.php | 5 + app/openapi/middleware/Auth.php | 31 ++++ app/openapi/model/OAuthAccessTokenModel.php | 32 ++++ app/openapi/model/OAuthAuthCodeModel.php | 33 ++++ app/openapi/model/OAuthClientModel.php | 33 ++++ app/openapi/model/OAuthRefreshTokenModel.php | 32 ++++ app/openapi/route/v1.php | 35 ++++ composer.json | 3 +- .../20250519083010_create_oauth_client.php | 42 +++++ .../20250519083050_create_oauth_auth_code.php | 41 +++++ ...250519083107_create_oauth_access_token.php | 40 +++++ ...50520014709_create_oauth_refresh_token.php | 40 +++++ extend/oauth/OAuthStorage.php | 167 ++++++++++++++++++ 16 files changed, 577 insertions(+), 1 deletion(-) create mode 100644 app/openapi/common.php create mode 100644 app/openapi/controller/v1/Authorize.php create mode 100644 app/openapi/event.php create mode 100644 app/openapi/middleware.php create mode 100644 app/openapi/middleware/Auth.php create mode 100644 app/openapi/model/OAuthAccessTokenModel.php create mode 100644 app/openapi/model/OAuthAuthCodeModel.php create mode 100644 app/openapi/model/OAuthClientModel.php create mode 100644 app/openapi/model/OAuthRefreshTokenModel.php create mode 100644 app/openapi/route/v1.php create mode 100644 database/migrations/20250519083010_create_oauth_client.php create mode 100644 database/migrations/20250519083050_create_oauth_auth_code.php create mode 100644 database/migrations/20250519083107_create_oauth_access_token.php create mode 100644 database/migrations/20250520014709_create_oauth_refresh_token.php create mode 100644 extend/oauth/OAuthStorage.php diff --git a/app/openapi/common.php b/app/openapi/common.php new file mode 100644 index 00000000..12436156 --- /dev/null +++ b/app/openapi/common.php @@ -0,0 +1,2 @@ +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()); + } + } +} \ No newline at end of file diff --git a/app/openapi/event.php b/app/openapi/event.php new file mode 100644 index 00000000..4eff8908 --- /dev/null +++ b/app/openapi/event.php @@ -0,0 +1,5 @@ +getBearerToken(); + $oauth->verifyAccessToken($token); + } catch (OAuth2ServerException $e) { + return error('Unauthorized', $e->sendHttpResponse(), 401); + } + + return $next($request); + } +} diff --git a/app/openapi/model/OAuthAccessTokenModel.php b/app/openapi/model/OAuthAccessTokenModel.php new file mode 100644 index 00000000..9c640db9 --- /dev/null +++ b/app/openapi/model/OAuthAccessTokenModel.php @@ -0,0 +1,32 @@ + '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); + } +} diff --git a/app/openapi/model/OAuthAuthCodeModel.php b/app/openapi/model/OAuthAuthCodeModel.php new file mode 100644 index 00000000..43d23aba --- /dev/null +++ b/app/openapi/model/OAuthAuthCodeModel.php @@ -0,0 +1,33 @@ + '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); + } +} diff --git a/app/openapi/model/OAuthClientModel.php b/app/openapi/model/OAuthClientModel.php new file mode 100644 index 00000000..dc126189 --- /dev/null +++ b/app/openapi/model/OAuthClientModel.php @@ -0,0 +1,33 @@ + '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); + } +} diff --git a/app/openapi/model/OAuthRefreshTokenModel.php b/app/openapi/model/OAuthRefreshTokenModel.php new file mode 100644 index 00000000..c24bc00d --- /dev/null +++ b/app/openapi/model/OAuthRefreshTokenModel.php @@ -0,0 +1,32 @@ + '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); + } +} diff --git a/app/openapi/route/v1.php b/app/openapi/route/v1.php new file mode 100644 index 00000000..765b966d --- /dev/null +++ b/app/openapi/route/v1.php @@ -0,0 +1,35 @@ +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); + }, +]); \ No newline at end of file diff --git a/composer.json b/composer.json index 5f3d933c..049a9ea8 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ "topthink/think-throttle": "^2.0", "intervention/image": "^3.10", "topthink/think-cors": "^1.0", - "phpoffice/phpspreadsheet": "^3.8" + "phpoffice/phpspreadsheet": "^3.8", + "friendsofsymfony/oauth2-php": "^1.3" }, "require-dev": { "symfony/var-dumper": ">=4.2", diff --git a/database/migrations/20250519083010_create_oauth_client.php b/database/migrations/20250519083010_create_oauth_client.php new file mode 100644 index 00000000..57b28a57 --- /dev/null +++ b/database/migrations/20250519083010_create_oauth_client.php @@ -0,0 +1,42 @@ +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(); + } +} diff --git a/database/migrations/20250519083050_create_oauth_auth_code.php b/database/migrations/20250519083050_create_oauth_auth_code.php new file mode 100644 index 00000000..4c34f484 --- /dev/null +++ b/database/migrations/20250519083050_create_oauth_auth_code.php @@ -0,0 +1,41 @@ +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(); + } +} diff --git a/database/migrations/20250519083107_create_oauth_access_token.php b/database/migrations/20250519083107_create_oauth_access_token.php new file mode 100644 index 00000000..633aab7b --- /dev/null +++ b/database/migrations/20250519083107_create_oauth_access_token.php @@ -0,0 +1,40 @@ +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(); + } +} diff --git a/database/migrations/20250520014709_create_oauth_refresh_token.php b/database/migrations/20250520014709_create_oauth_refresh_token.php new file mode 100644 index 00000000..7c181584 --- /dev/null +++ b/database/migrations/20250520014709_create_oauth_refresh_token.php @@ -0,0 +1,40 @@ +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(); + } +} diff --git a/extend/oauth/OAuthStorage.php b/extend/oauth/OAuthStorage.php new file mode 100644 index 00000000..ade0f5d7 --- /dev/null +++ b/extend/oauth/OAuthStorage.php @@ -0,0 +1,167 @@ +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(); + } +} From 9ecc9ac9574407c9602d953ceaabdc289e8ef63b Mon Sep 17 00:00:00 2001 From: jsasg <735273025@qq.com> Date: Tue, 20 May 2025 14:41:30 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E5=BC=80=E6=94=BEAPI=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/openapi/controller/v1/Product.php | 174 ++++++++++++++++++ app/openapi/controller/v1/ProductCategory.php | 51 +++++ app/openapi/model/LanguageModel.php | 19 ++ app/openapi/model/ProductCategoryModel.php | 33 ++++ app/openapi/model/ProductModel.php | 62 +++++++ app/openapi/model/ProductParamsModel.php | 15 ++ .../model/ProductPurchaseLinkModel.php | 19 ++ .../model/ProductPurchasePlatformModel.php | 15 ++ app/openapi/model/ProductRelatedModel.php | 19 ++ app/openapi/model/ProductSkuAttrModel.php | 19 ++ app/openapi/model/ProductSkuModel.php | 19 ++ app/openapi/route/v1.php | 4 +- 12 files changed, 447 insertions(+), 2 deletions(-) create mode 100644 app/openapi/controller/v1/Product.php create mode 100644 app/openapi/controller/v1/ProductCategory.php create mode 100644 app/openapi/model/LanguageModel.php create mode 100644 app/openapi/model/ProductCategoryModel.php create mode 100644 app/openapi/model/ProductModel.php create mode 100644 app/openapi/model/ProductParamsModel.php create mode 100644 app/openapi/model/ProductPurchaseLinkModel.php create mode 100644 app/openapi/model/ProductPurchasePlatformModel.php create mode 100644 app/openapi/model/ProductRelatedModel.php create mode 100644 app/openapi/model/ProductSkuAttrModel.php create mode 100644 app/openapi/model/ProductSkuModel.php diff --git a/app/openapi/controller/v1/Product.php b/app/openapi/controller/v1/Product.php new file mode 100644 index 00000000..1a33bc15 --- /dev/null +++ b/app/openapi/controller/v1/Product.php @@ -0,0 +1,174 @@ +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); + } +} diff --git a/app/openapi/controller/v1/ProductCategory.php b/app/openapi/controller/v1/ProductCategory.php new file mode 100644 index 00000000..98722c89 --- /dev/null +++ b/app/openapi/controller/v1/ProductCategory.php @@ -0,0 +1,51 @@ +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', + 'deleted_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); + } +} diff --git a/app/openapi/model/LanguageModel.php b/app/openapi/model/LanguageModel.php new file mode 100644 index 00000000..d2a008b6 --- /dev/null +++ b/app/openapi/model/LanguageModel.php @@ -0,0 +1,19 @@ +where('code', '=', $code); + } +} diff --git a/app/openapi/model/ProductCategoryModel.php b/app/openapi/model/ProductCategoryModel.php new file mode 100644 index 00000000..3769953a --- /dev/null +++ b/app/openapi/model/ProductCategoryModel.php @@ -0,0 +1,33 @@ +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); + } +} diff --git a/app/openapi/model/ProductModel.php b/app/openapi/model/ProductModel.php new file mode 100644 index 00000000..918566c5 --- /dev/null +++ b/app/openapi/model/ProductModel.php @@ -0,0 +1,62 @@ +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); + } +} diff --git a/app/openapi/model/ProductParamsModel.php b/app/openapi/model/ProductParamsModel.php new file mode 100644 index 00000000..0b88a828 --- /dev/null +++ b/app/openapi/model/ProductParamsModel.php @@ -0,0 +1,15 @@ +belongsTo(ProductPurchasePlatformModel::class, 'platform_id', 'id')->bind(['platform']); + } +} diff --git a/app/openapi/model/ProductPurchasePlatformModel.php b/app/openapi/model/ProductPurchasePlatformModel.php new file mode 100644 index 00000000..ab24cc2a --- /dev/null +++ b/app/openapi/model/ProductPurchasePlatformModel.php @@ -0,0 +1,15 @@ +belongsTo(ProductModel::class, 'related_product_id', 'id'); + } +} diff --git a/app/openapi/model/ProductSkuAttrModel.php b/app/openapi/model/ProductSkuAttrModel.php new file mode 100644 index 00000000..b88524c0 --- /dev/null +++ b/app/openapi/model/ProductSkuAttrModel.php @@ -0,0 +1,19 @@ +belongsTo(\app\common\model\ProductAttrBaseModel::class, 'attr_id', 'id')->bind(['attr_name']); + } +} diff --git a/app/openapi/model/ProductSkuModel.php b/app/openapi/model/ProductSkuModel.php new file mode 100644 index 00000000..d75680cb --- /dev/null +++ b/app/openapi/model/ProductSkuModel.php @@ -0,0 +1,19 @@ +hasMany(ProductSkuAttrModel::class, 'sku_id', 'id'); + } +} diff --git a/app/openapi/route/v1.php b/app/openapi/route/v1.php index 765b966d..1c5fc6b7 100644 --- a/app/openapi/route/v1.php +++ b/app/openapi/route/v1.php @@ -12,10 +12,10 @@ Route::group('v1', function() { Route::get('products', 'v1.Product/list'); Route::group('product', function() { // 获取产品信息 - Route::get(':id', 'v1.Product/detail'); + Route::get(':id', 'v1.Product/detail')->when('id', 'number'); // 获取产品分类 - Route::get('categories', 'v1.Product/categories'); + Route::get('categories', 'v1.ProductCategory/list'); }); // 获取新闻动态 From e712030e3e62b50d3d53b67a3e667e1911f9405c Mon Sep 17 00:00:00 2001 From: jsasg <735273025@qq.com> Date: Wed, 21 May 2025 16:59:48 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E5=BC=80=E6=94=BEAPI=E6=96=87?= =?UTF-8?q?=E7=AB=A0=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/openapi/common.php | 68 +++++++++++++ app/openapi/controller/v1/Article.php | 96 +++++++++++++++++++ app/openapi/controller/v1/ArticleCategory.php | 50 ++++++++++ app/openapi/controller/v1/ProductCategory.php | 3 +- app/openapi/model/ArticleCategoryModel.php | 34 +++++++ app/openapi/model/ArticleModel.php | 40 ++++++++ app/openapi/route/v1.php | 16 ++-- 7 files changed, 299 insertions(+), 8 deletions(-) create mode 100644 app/openapi/controller/v1/Article.php create mode 100644 app/openapi/controller/v1/ArticleCategory.php create mode 100644 app/openapi/model/ArticleCategoryModel.php create mode 100644 app/openapi/model/ArticleModel.php diff --git a/app/openapi/common.php b/app/openapi/common.php index 12436156..e31187a6 100644 --- a/app/openapi/common.php +++ b/app/openapi/common.php @@ -1,2 +1,70 @@ ]+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 + ); + } +} diff --git a/app/openapi/controller/v1/Article.php b/app/openapi/controller/v1/Article.php new file mode 100644 index 00000000..72cafb37 --- /dev/null +++ b/app/openapi/controller/v1/Article.php @@ -0,0 +1,96 @@ +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); + } +} diff --git a/app/openapi/controller/v1/ArticleCategory.php b/app/openapi/controller/v1/ArticleCategory.php new file mode 100644 index 00000000..42d65b8e --- /dev/null +++ b/app/openapi/controller/v1/ArticleCategory.php @@ -0,0 +1,50 @@ +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); + } +} diff --git a/app/openapi/controller/v1/ProductCategory.php b/app/openapi/controller/v1/ProductCategory.php index 98722c89..183d7d1e 100644 --- a/app/openapi/controller/v1/ProductCategory.php +++ b/app/openapi/controller/v1/ProductCategory.php @@ -29,8 +29,7 @@ class ProductCategory 'unique_id', 'related_tco_category', 'created_at', - 'updated_at', - 'deleted_at' + 'updated_at' ]) ->language($params['language']??'zh-cn') ->parent($params['parent_id']??null) diff --git a/app/openapi/model/ArticleCategoryModel.php b/app/openapi/model/ArticleCategoryModel.php new file mode 100644 index 00000000..d10d8f02 --- /dev/null +++ b/app/openapi/model/ArticleCategoryModel.php @@ -0,0 +1,34 @@ +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); + } +} diff --git a/app/openapi/model/ArticleModel.php b/app/openapi/model/ArticleModel.php new file mode 100644 index 00000000..3a78374f --- /dev/null +++ b/app/openapi/model/ArticleModel.php @@ -0,0 +1,40 @@ +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); + } +} diff --git a/app/openapi/route/v1.php b/app/openapi/route/v1.php index 1c5fc6b7..63ed4d95 100644 --- a/app/openapi/route/v1.php +++ b/app/openapi/route/v1.php @@ -18,11 +18,14 @@ Route::group('v1', function() { Route::get('categories', 'v1.ProductCategory/list'); }); - // 获取新闻动态 - Route::get('news', 'v1.News/list'); - Route::group('news', function() { - // 获取新闻详情 - Route::get(':id', 'v1.News/detail'); + // 获取文章动态 + 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); @@ -32,4 +35,5 @@ Route::group('v1', function() { 'visit_fail_response' => function (\think\middleware\Throttle $throttle, \think\Request $request, int $wait_seconds) { return \think\Response::create('您的操作过于频繁, 请在 ' . $wait_seconds . ' 秒后再试。')->code(429); }, -]); \ No newline at end of file +]) +->completeMatch(true); \ No newline at end of file From 8b5cf24224be0a44c530f33a755b2381fb705f0f Mon Sep 17 00:00:00 2001 From: jsasg <735273025@qq.com> Date: Thu, 22 May 2025 14:51:10 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E6=96=87=E7=AB=A0=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E9=81=97=E6=BC=8F=E7=82=B9=E5=87=BB=E9=93=BE=E6=8E=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/index/view/article/detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/index/view/article/detail.html b/app/index/view/article/detail.html index 728e0948..a852fa02 100644 --- a/app/index/view/article/detail.html +++ b/app/index/view/article/detail.html @@ -69,7 +69,7 @@