ソースを参照

✨ feat(api): 添加公共帖子API端点

- 新增公共路由模块,支持未授权访问的帖子接口
- 实现热门帖子查询接口,按点赞数和创建时间排序
- 添加PublicRoutes类型定义,完善API类型系统

✨ feat(client): 实现公共帖子客户端及首页集成

- 新增publicPostClient客户端,用于访问公开帖子API
- 修改HomePage组件,使用公共API获取热门帖子
- 调整帖子查询逻辑,从公开接口获取精选内容

♻️ refactor(entity): 优化用户与帖子实体关系

- 修复UserEntity与PostEntity关联关系定义
- 在UserEntity中添加posts反向关联
- 修正PostEntity中user关联的外键引用
yourname 5 ヶ月 前
コミット
0df0ab4982

+ 5 - 1
src/client/api.ts

@@ -1,7 +1,7 @@
 import axios, { isAxiosError } from 'axios';
 import { hc } from 'hono/client'
 import type {
-  AuthRoutes, UserRoutes, RoleRoutes, PostRoutes
+  AuthRoutes, UserRoutes, RoleRoutes, PostRoutes, PublicRoutes
 } from '@/server/api';
 
 // 创建 axios 适配器
@@ -74,3 +74,7 @@ export const roleClient = hc<RoleRoutes>('/', {
 export const postClient = hc<PostRoutes>('/', {
   fetch: axiosFetch,
 }).api.v1.posts;
+
+export const publicPostClient = hc<PublicRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.public.posts;

+ 2 - 2
src/client/home/pages/HomePage.tsx

@@ -1,7 +1,7 @@
 import React, { useEffect, useState } from 'react';
 import { UserOutlined, MessageOutlined, HeartOutlined, ArrowRightOutlined, TwitterOutlined, InstagramOutlined, FacebookOutlined } from '@ant-design/icons';
 import { useNavigate } from 'react-router-dom';
-import { postClient } from '@/client/api';
+import { postClient, publicPostClient } from '@/client/api';
 import { InferResponseType } from 'hono/client';
 
 // 定义帖子类型
@@ -57,7 +57,7 @@ const HomePage: React.FC = () => {
     const fetchFeaturedPosts = async () => {
       try {
         setLoading(true);
-        const response = await postClient.$get({
+        const response = await publicPostClient.featured.$get({
           query: {
             page: 1,
             pageSize: 3,

+ 4 - 0
src/server/api.ts

@@ -3,6 +3,7 @@ import { errorHandler } from './utils/errorHandler'
 import usersRouter from './api/users/index'
 import authRoute from './api/auth/index'
 import rolesRoute from './api/roles/index'
+import publicRoute from './api/public/index';
 import postsRouter from './api/posts/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
@@ -55,10 +56,13 @@ const userRoutes = api.route('/api/v1/users', usersRouter)
 const authRoutes = api.route('/api/v1/auth', authRoute)
 const roleRoutes = api.route('/api/v1/roles', rolesRoute)
 const postRoutes = api.route('/api/v1/posts', postsRouter)
+// 注册公共路由
+const publicRoutes = api.route('/api/v1/public', publicRoute);
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
 export type RoleRoutes = typeof roleRoutes
 export type PostRoutes = typeof postRoutes
+export type PublicRoutes = typeof publicRoutes
 
 export default api

+ 7 - 0
src/server/api/public/index.ts

@@ -0,0 +1,7 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import featuredPostsRoute from './posts/featured';
+
+const app = new OpenAPIHono()
+  .route('/posts', featuredPostsRoute);
+
+export default app;

+ 76 - 0
src/server/api/public/posts/featured.ts

@@ -0,0 +1,76 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { PostService } from '@/server/modules/posts/post.service';
+import { PostSchema } from '@/server/modules/posts/post.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+
+// 查询参数Schema
+const QuerySchema = z.object({
+  page: z.coerce.number().int().positive().default(1).openapi({
+    example: 1,
+    description: '页码'
+  }),
+  pageSize: z.coerce.number().int().positive().default(10).openapi({
+    example: 10,
+    description: '每页条数'
+  })
+});
+
+// 响应Schema
+const PopularPostsResponse = z.object({
+  data: z.array(PostSchema),
+  pagination: z.object({
+    total: z.number().openapi({ example: 100, description: '总记录数' }),
+    current: z.number().openapi({ example: 1, description: '当前页码' }),
+    pageSize: z.number().openapi({ example: 10, description: '每页数量' })
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'get',
+  path: '/featured',
+  // 不添加authMiddleware,确保公开访问
+  request: {
+    query: QuerySchema
+  },
+  responses: {
+    200: {
+      description: '成功获取热门帖子列表',
+      content: { 'application/json': { schema: PopularPostsResponse } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono().openapi(routeDef, async (c) => {
+  try {
+    const { page, pageSize } = c.req.valid('query');
+    const postService = new PostService(AppDataSource);
+    
+    const [posts, total] = await postService.getPopularPosts(page, pageSize);
+    
+    return c.json({
+      data: posts,
+      pagination: {
+        total,
+        current: page,
+        pageSize
+      }
+    }, 200);
+  } catch (error) {
+    const { code = 500, message = '获取热门帖子失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code as unknown as 400 | 500);
+  }
+});
+
+export default app;

+ 1 - 1
src/server/modules/posts/post.entity.ts

@@ -26,7 +26,7 @@ export class PostEntity {
   @Column({ name: 'is_deleted', type: 'int', default: DeleteStatus.NOT_DELETED, comment: '是否删除(0:未删除,1:已删除)' })
   isDeleted!: DeleteStatus;
 
-  @ManyToOne(() => UserEntity, user => user.id)
+  @ManyToOne(() => UserEntity, user => user.posts)
   user!: UserEntity;
 
   @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })

+ 20 - 0
src/server/modules/posts/post.service.ts

@@ -217,4 +217,24 @@ export class PostService {
     
     return [[], 0];
   }
+  /**
+   * 获取热门帖子(按点赞数和创建时间排序)
+   */
+  async getPopularPosts(page: number = 1, pageSize: number = 10): Promise<[Post[], number]> {
+    const skip = (page - 1) * pageSize;
+    
+    const [posts, total] = await this.postRepository.findAndCount({
+      where: { isDeleted: DeleteStatus.NOT_DELETED },
+      relations: ['user'],
+      skip,
+      take: pageSize,
+      order: { 
+        likesCount: 'DESC', 
+        createdAt: 'DESC' 
+      }
+    });
+
+    return [posts, total];
+  }
+
 }

+ 4 - 0
src/server/modules/users/user.entity.ts

@@ -1,4 +1,5 @@
 import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
+import { PostEntity } from '../posts/post.entity';
 import { Role, RoleSchema } from './role.entity';
 import { z } from '@hono/zod-openapi';
 import { DeleteStatus, DisabledStatus } from '@/share/types';
@@ -55,6 +56,9 @@ export class UserEntity {
   @OneToMany(() => FollowEntity, follow => follow.following)
   followers!: FollowEntity[];
 
+  @OneToMany(() => PostEntity, post => post.user)
+  posts!: PostEntity[];
+
   @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
   createdAt!: Date;