Przeglądaj źródła

✨ feat(frontend): add login page component

- create login page with form validation
- implement authentication logic with API integration
- add redirect after successful login
- include "register account" button with navigation

✨ feat(server): add post and follow functionality

- create post entity with content and image support
- implement post CRUD operations and like functionality
- add follow/unfollow user relationships
- update data source to include new entities
- add API routes for posts and follows

✨ feat(user): add user profile fields

- add bio, location and website fields to user entity
- update user schema to include new profile information
yourname 4 miesięcy temu
rodzic
commit
b7118dee44

+ 111 - 0
src/client/home/pages/LoginPage.tsx

@@ -0,0 +1,111 @@
+import React, { useState } from 'react';
+import { Form, Input, Button, Card, Typography, message, Divider } from 'antd';
+import { UserOutlined, LockOutlined } from '@ant-design/icons';
+import { Link, useNavigate } from 'react-router-dom';
+import { authClient } from '@/client/api';
+import { useAuth } from '@/client/admin/hooks/AuthProvider';
+
+const { Title } = Typography;
+
+const LoginPage: React.FC = () => {
+  const [loading, setLoading] = useState(false);
+  const [form] = Form.useForm();
+  const { login } = useAuth();
+  const navigate = useNavigate();
+
+  const handleSubmit = async (values: any) => {
+    try {
+      setLoading(true);
+      
+      // 调用登录API
+      const response = await authClient.login.password.$post({
+        json: {
+          username: values.username,
+          password: values.password
+        }
+      });
+      
+      if (!response.ok) {
+        const errorData = await response.json();
+        throw new Error(errorData.message || '登录失败');
+      }
+      
+      const data = await response.json();
+      
+      // 保存token并更新认证状态
+      localStorage.setItem('token', data.token);
+      await login(data.user, data.token);
+      
+      message.success('登录成功');
+      navigate('/');
+    } catch (error) {
+      console.error('Login error:', error);
+      message.error((error as Error).message || '登录失败,请检查用户名和密码');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="flex justify-center items-center min-h-screen bg-gray-50">
+      <Card className="w-full max-w-md shadow-lg">
+        <div className="text-center mb-6">
+          <Title level={2}>社交媒体平台</Title>
+          <p className="text-gray-500">登录您的账号</p>
+        </div>
+        
+        <Form
+          form={form}
+          name="login_form"
+          layout="vertical"
+          onFinish={handleSubmit}
+        >
+          <Form.Item
+            name="username"
+            label="用户名"
+            rules={[{ required: true, message: '请输入用户名' }]}
+          >
+            <Input 
+              prefix={<UserOutlined className="text-primary" />} 
+              placeholder="请输入用户名" 
+            />
+          </Form.Item>
+          
+          <Form.Item
+            name="password"
+            label="密码"
+            rules={[{ required: true, message: '请输入密码' }]}
+          >
+            <Input.Password
+              prefix={<LockOutlined className="text-primary" />}
+              placeholder="请输入密码"
+            />
+          </Form.Item>
+          
+          <Form.Item>
+            <Button 
+              type="primary" 
+              htmlType="submit" 
+              className="w-full h-10 text-base"
+              loading={loading}
+            >
+              登录
+            </Button>
+          </Form.Item>
+        </Form>
+        
+        <Divider>还没有账号?</Divider>
+        
+        <Button 
+          type="default" 
+          className="w-full"
+          onClick={() => navigate('/register')}
+        >
+          注册账号
+        </Button>
+      </Card>
+    </div>
+  );
+};
+
+export default LoginPage;

+ 3 - 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 postsRouter from './api/posts/index'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 
@@ -53,9 +54,11 @@ if(!import.meta.env.PROD){
 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)
 
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
 export type RoleRoutes = typeof roleRoutes
+export type PostRoutes = typeof postRoutes
 
 export default api

+ 19 - 0
src/server/api/posts/index.ts

@@ -0,0 +1,19 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import listPostsRoute from './get';
+import createPostRoute from './post';
+import getPostByIdRoute from './[id]/get';
+import updatePostRoute from './[id]/put';
+import deletePostRoute from './[id]/delete';
+import likePostRoute from './[id]/like/post';
+import unlikePostRoute from './[id]/like/delete';
+
+const app = new OpenAPIHono()
+  .route('/', listPostsRoute)
+  .route('/', createPostRoute)
+  .route('/', getPostByIdRoute)
+  .route('/', updatePostRoute)
+  .route('/', deletePostRoute)
+  .route('/', likePostRoute)
+  .route('/', unlikePostRoute);
+
+export default app;

+ 57 - 0
src/server/api/posts/post.ts

@@ -0,0 +1,57 @@
+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 { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { CreatePostDto, PostSchema } from '@/server/modules/posts/post.entity';
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'post',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: CreatePostDto }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '成功创建帖子',
+      content: { 'application/json': { schema: PostSchema } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '用户不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const data = await c.req.json();
+    const user = c.get('user');
+    const postService = new PostService(AppDataSource);
+    
+    const result = await postService.createPost(user.id, data);
+    return c.json(result, 200);
+  } catch (error) {
+    const { code = 500, message = '创建帖子失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 79 - 0
src/server/api/users/[id]/follow/delete.ts

@@ -0,0 +1,79 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import type { StatusCode } from 'hono/utils/http-status';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { FollowService } from '@/server/modules/follows/follow.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+
+// 参数Schema
+const ParamsSchema = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 2,
+    description: '被取消关注用户ID'
+  })
+});
+
+// 响应Schema
+const SuccessSchema = z.object({
+  success: z.boolean().openapi({
+    example: true,
+    description: '操作是否成功'
+  }),
+  message: z.string().openapi({
+    example: '取消关注成功',
+    description: '操作结果消息'
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'delete',
+  path: '/{id}/follow',
+  middleware: [authMiddleware],
+  request: {
+    params: ParamsSchema
+  },
+  responses: {
+    200: {
+      description: '成功取消关注用户',
+      content: { 'application/json': { schema: SuccessSchema } }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '用户不存在或未关注',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const params = c.req.valid('param');
+    const user = c.get('user');
+    const followService = new FollowService(AppDataSource);
+    
+    const result = await followService.unfollowUser(user.id, params.id);
+    
+    if (!result) {
+      return c.json({ code: 404, message: '未关注该用户' }, 404);
+    }
+    
+    return c.json({ success: true, message: '取消关注成功' }, 200);
+  } catch (error) {
+    const { code = 500, message = '取消关注失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code as StatusCode);
+  }
+});
+
+export default app;

+ 62 - 0
src/server/api/users/[id]/follow/post.ts

@@ -0,0 +1,62 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { FollowService } from '@/server/modules/follows/follow.service';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { FollowSchema } from '@/server/modules/follows/follow.entity';
+
+// 参数Schema
+const ParamsSchema = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 2,
+    description: '被关注用户ID'
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'post',
+  path: '/{id}/follow',
+  middleware: [authMiddleware],
+  request: {
+    params: ParamsSchema
+  },
+  responses: {
+    200: {
+      description: '成功关注用户',
+      content: { 'application/json': { schema: FollowSchema } }
+    },
+    400: {
+      description: '请求参数错误或已关注该用户',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    404: {
+      description: '用户不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 路由实现
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const params = c.req.valid('param');
+    const user = c.get('user');
+    const followService = new FollowService(AppDataSource);
+    
+    const result = await followService.followUser(user.id, params.id);
+    return c.json(result, 200);
+  } catch (error) {
+    const { code = 500, message = '关注用户失败' } = error as Error & { code?: number };
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 9 - 1
src/server/api/users/index.ts

@@ -4,12 +4,20 @@ import createUserRoute from './post';
 import getUserByIdRoute from './[id]/get';
 import updateUserRoute from './[id]/put';
 import deleteUserRoute from './[id]/delete';
+import followUserRoute from './[id]/follow/post';
+import unfollowUserRoute from './[id]/follow/delete';
+import getFollowingRoute from './[id]/following/get';
+import getFollowersRoute from './[id]/followers/get';
 
 const app = new OpenAPIHono()
   .route('/', listUsersRoute)
   .route('/', createUserRoute)
   .route('/', getUserByIdRoute)
   .route('/', updateUserRoute)
-  .route('/', deleteUserRoute);
+  .route('/', deleteUserRoute)
+  .route('/', followUserRoute)
+  .route('/', unfollowUserRoute)
+  .route('/', getFollowingRoute)
+  .route('/', getFollowersRoute);
 
 export default app;

+ 3 - 1
src/server/data-source.ts

@@ -5,6 +5,8 @@ import process from 'node:process'
 // 实体类导入
 import { UserEntity as User } from "./modules/users/user.entity"
 import { Role } from "./modules/users/role.entity"
+import { FollowEntity as Follow } from "./modules/follows/follow.entity"
+import { PostEntity as Post } from "./modules/posts/post.entity"
 
 export const AppDataSource = new DataSource({
   type: "mysql",
@@ -14,7 +16,7 @@ export const AppDataSource = new DataSource({
   password: process.env.DB_PASSWORD || "",
   database: process.env.DB_DATABASE || "d8dai",
   entities: [
-    User, Role
+    User, Role, Follow, Post
   ],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 52 - 0
src/server/modules/follows/follow.entity.ts

@@ -0,0 +1,52 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+import { UserEntity } from '../users/user.entity';
+
+@Entity({ name: 'follows' })
+export class FollowEntity {
+  @PrimaryGeneratedColumn({ unsigned: true, comment: '关注关系ID' })
+  id!: number;
+
+  @Column({ name: 'follower_id', type: 'int', unsigned: true, comment: '关注者ID' })
+  followerId!: number;
+
+  @Column({ name: 'following_id', type: 'int', unsigned: true, comment: '被关注者ID' })
+  followingId!: number;
+
+  @ManyToOne(() => UserEntity, user => user.id)
+  follower!: UserEntity;
+
+  @ManyToOne(() => UserEntity, user => user.id)
+  following!: UserEntity;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '关注时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  constructor(partial?: Partial<FollowEntity>) {
+    Object.assign(this, partial);
+  }
+}
+
+export const FollowSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '关注关系ID' }),
+  followerId: z.number().int().positive().openapi({
+    example: 1,
+    description: '关注者ID'
+  }),
+  followingId: z.number().int().positive().openapi({
+    example: 2,
+    description: '被关注者ID'
+  }),
+  createdAt: z.date().openapi({ description: '关注时间' }),
+  updatedAt: z.date().openapi({ description: '更新时间' })
+});
+
+export const CreateFollowDto = z.object({
+  followingId: z.number().int().positive().openapi({
+    example: 2,
+    description: '被关注者ID'
+  })
+});

+ 123 - 0
src/server/modules/follows/follow.service.ts

@@ -0,0 +1,123 @@
+import { DataSource, Repository } from 'typeorm';
+import { FollowEntity as Follow } from './follow.entity';
+import { UserEntity as User } from '../users/user.entity';
+import { HTTPException } from 'hono/http-exception';
+
+export class FollowService {
+  private followRepository: Repository<Follow>;
+  private userRepository: Repository<User>;
+
+  constructor(dataSource: DataSource) {
+    this.followRepository = dataSource.getRepository(Follow);
+    this.userRepository = dataSource.getRepository(User);
+  }
+
+  /**
+   * 关注用户
+   */
+  async followUser(followerId: number, followingId: number): Promise<Follow> {
+    // 检查不能关注自己
+    if (followerId === followingId) {
+      throw new HTTPException(400, { message: '不能关注自己' });
+    }
+
+    // 检查用户是否存在
+    const followingUser = await this.userRepository.findOneBy({ id: followingId });
+    if (!followingUser) {
+      throw new HTTPException(404, { message: '被关注用户不存在' });
+    }
+
+    // 检查是否已经关注
+    const existingFollow = await this.followRepository.findOneBy({
+      followerId,
+      followingId
+    });
+    if (existingFollow) {
+      throw new HTTPException(400, { message: '已经关注该用户' });
+    }
+
+    // 创建关注关系
+    const follow = this.followRepository.create({
+      followerId,
+      followingId
+    });
+
+    return this.followRepository.save(follow);
+  }
+
+  /**
+   * 取消关注用户
+   */
+  async unfollowUser(followerId: number, followingId: number): Promise<boolean> {
+    const result = await this.followRepository.delete({
+      followerId,
+      followingId
+    });
+
+    return (result.affected || 0) > 0;
+  }
+
+  /**
+   * 获取用户关注列表
+   */
+  async getFollowing(followerId: number, page: number = 1, pageSize: number = 10): Promise<[Follow[], number]> {
+    const skip = (page - 1) * pageSize;
+    
+    const [follows, total] = await this.followRepository.findAndCount({
+      where: { followerId },
+      relations: ['following'],
+      skip,
+      take: pageSize,
+      order: { createdAt: 'DESC' }
+    });
+
+    return [follows, total];
+  }
+
+  /**
+   * 获取用户粉丝列表
+   */
+  async getFollowers(followingId: number, page: number = 1, pageSize: number = 10): Promise<[Follow[], number]> {
+    const skip = (page - 1) * pageSize;
+    
+    const [follows, total] = await this.followRepository.findAndCount({
+      where: { followingId },
+      relations: ['follower'],
+      skip,
+      take: pageSize,
+      order: { createdAt: 'DESC' }
+    });
+
+    return [follows, total];
+  }
+
+  /**
+   * 检查是否关注了用户
+   */
+  async isFollowing(followerId: number, followingId: number): Promise<boolean> {
+    const follow = await this.followRepository.findOneBy({
+      followerId,
+      followingId
+    });
+
+    return !!follow;
+  }
+
+  /**
+   * 获取用户关注数量
+   */
+  async getFollowingCount(userId: number): Promise<number> {
+    return this.followRepository.count({
+      where: { followerId: userId }
+    });
+  }
+
+  /**
+   * 获取用户粉丝数量
+   */
+  async getFollowersCount(userId: number): Promise<number> {
+    return this.followRepository.count({
+      where: { followingId: userId }
+    });
+  }
+}

+ 89 - 0
src/server/modules/posts/post.entity.ts

@@ -0,0 +1,89 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+import { UserEntity } from '../users/user.entity';
+import { DeleteStatus } from '@/share/types';
+
+@Entity({ name: 'posts' })
+export class PostEntity {
+  @PrimaryGeneratedColumn({ unsigned: true, comment: '帖子ID' })
+  id!: number;
+
+  @Column({ name: 'user_id', type: 'int', unsigned: true, comment: '用户ID' })
+  userId!: number;
+
+  @Column({ name: 'content', type: 'text', comment: '帖子内容' })
+  content!: string;
+
+  @Column({ name: 'images', type: 'json', nullable: true, comment: '图片URL列表' })
+  images!: string[] | null;
+
+  @Column({ name: 'likes_count', type: 'int', default: 0, comment: '点赞数' })
+  likesCount!: number;
+
+  @Column({ name: 'comments_count', type: 'int', default: 0, comment: '评论数' })
+  commentsCount!: number;
+
+  @Column({ name: 'is_deleted', type: 'int', default: DeleteStatus.NOT_DELETED, comment: '是否删除(0:未删除,1:已删除)' })
+  isDeleted!: DeleteStatus;
+
+  @ManyToOne(() => UserEntity, user => user.id)
+  user!: UserEntity;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp', comment: '创建时间' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp', comment: '更新时间' })
+  updatedAt!: Date;
+
+  constructor(partial?: Partial<PostEntity>) {
+    Object.assign(this, partial);
+  }
+}
+
+export const PostSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '帖子ID' }),
+  userId: z.number().int().positive().openapi({
+    example: 1,
+    description: '用户ID'
+  }),
+  content: z.string().openapi({
+    example: '这是一条新动态',
+    description: '帖子内容'
+  }),
+  images: z.array(z.string().url()).nullable().openapi({
+    example: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg'],
+    description: '图片URL列表'
+  }),
+  likesCount: z.number().int().nonnegative().openapi({
+    example: 10,
+    description: '点赞数'
+  }),
+  commentsCount: z.number().int().nonnegative().openapi({
+    example: 5,
+    description: '评论数'
+  }),
+  isDeleted: z.number().int().min(0).max(1).default(DeleteStatus.NOT_DELETED).openapi({
+    example: DeleteStatus.NOT_DELETED,
+    description: '是否删除(0:未删除,1:已删除)'
+  }),
+  createdAt: z.date().openapi({ description: '创建时间' }),
+  updatedAt: z.date().openapi({ description: '更新时间' })
+});
+
+export const CreatePostDto = z.object({
+  content: z.string().min(1).max(1000).openapi({
+    example: '这是一条新动态',
+    description: '帖子内容,1-1000个字符'
+  }),
+  images: z.array(z.string().url()).optional().openapi({
+    example: ['https://example.com/image1.jpg'],
+    description: '图片URL列表'
+  })
+});
+
+export const UpdatePostDto = z.object({
+  content: z.string().min(1).max(1000).optional().openapi({
+    example: '更新后的动态内容',
+    description: '帖子内容,1-1000个字符'
+  })
+});

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

@@ -0,0 +1,157 @@
+import { DataSource, Repository } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+import { PostEntity as Post } from './post.entity';
+import { UserEntity as User } from '../users/user.entity';
+import type { CreatePostDto, UpdatePostDto } from './post.entity';
+import { HTTPException } from 'hono/http-exception';
+import { DeleteStatus } from '@/share/types';
+
+export class PostService {
+  private postRepository: Repository<Post>;
+  private userRepository: Repository<User>;
+
+  constructor(dataSource: DataSource) {
+    this.postRepository = dataSource.getRepository(Post);
+    this.userRepository = dataSource.getRepository(User);
+  }
+
+  /**
+   * 创建帖子
+   */
+  async createPost(userId: number, data: z.infer<typeof CreatePostDto>): Promise<Post> {
+    // 验证用户是否存在
+    const user = await this.userRepository.findOneBy({ id: userId });
+    if (!user) {
+      throw new HTTPException(404, { message: '用户不存在' });
+    }
+
+    const post = this.postRepository.create({
+      userId,
+      ...data,
+      likesCount: 0,
+      commentsCount: 0
+    });
+
+    const savedPost = await this.postRepository.save(post);
+    return savedPost as unknown as Post;
+  }
+
+  /**
+   * 获取帖子详情
+   */
+  async getPostById(id: number): Promise<Post | null> {
+    const post = await this.postRepository.findOne({
+      where: { id, isDeleted: DeleteStatus.NOT_DELETED },
+      relations: ['user']
+    });
+
+    return post;
+  }
+
+  /**
+   * 更新帖子
+   */
+  async updatePost(postId: number, userId: number, data: z.infer<typeof UpdatePostDto>): Promise<Post | null> {
+    // 验证帖子是否存在且属于当前用户
+    const post = await this.postRepository.findOneBy({
+      id: postId,
+      userId,
+      isDeleted: DeleteStatus.NOT_DELETED
+    });
+
+    if (!post) {
+      throw new HTTPException(404, { message: '帖子不存在或没有权限' });
+    }
+
+    Object.assign(post, data);
+    return this.postRepository.save(post);
+  }
+
+  /**
+   * 删除帖子(软删除)
+   */
+  async deletePost(postId: number, userId: number): Promise<boolean> {
+    const result = await this.postRepository.update(
+      { id: postId, userId, isDeleted: DeleteStatus.NOT_DELETED },
+      { isDeleted: DeleteStatus.DELETED }
+    );
+
+    return (result.affected || 0) > 0;
+  }
+
+  /**
+   * 获取用户帖子列表
+   */
+  async getUserPosts(userId: number, page: number = 1, pageSize: number = 10): Promise<[Post[], number]> {
+    const skip = (page - 1) * pageSize;
+    
+    const [posts, total] = await this.postRepository.findAndCount({
+      where: { userId, isDeleted: DeleteStatus.NOT_DELETED },
+      relations: ['user'],
+      skip,
+      take: pageSize,
+      order: { createdAt: 'DESC' }
+    });
+
+    return [posts, total];
+  }
+
+  /**
+   * 获取关注用户的帖子流
+   */
+  async getFollowingFeed(followerId: number, page: number = 1, pageSize: number = 10): Promise<[Post[], number]> {
+    const skip = (page - 1) * pageSize;
+    
+    // 直接查询关注用户的帖子,假设follow表存在且有关联
+    const query = this.postRepository
+      .createQueryBuilder('post')
+      .leftJoin('follows', 'f', 'f.following_id = post.user_id')
+      .where('f.follower_id = :followerId', { followerId })
+      .andWhere('post.is_deleted = :notDeleted', { notDeleted: DeleteStatus.NOT_DELETED })
+      .leftJoinAndSelect('post.user', 'user')
+      .orderBy('post.created_at', 'DESC')
+      .skip(skip)
+      .take(pageSize);
+
+    const [posts, total] = await query.getManyAndCount();
+    return [posts, total];
+  }
+
+  /**
+   * 点赞帖子
+   */
+  async likePost(postId: number): Promise<Post | null> {
+    const post = await this.postRepository.findOneBy({
+      id: postId,
+      isDeleted: DeleteStatus.NOT_DELETED
+    });
+
+    if (!post) {
+      throw new HTTPException(404, { message: '帖子不存在' });
+    }
+
+    post.likesCount += 1;
+    return this.postRepository.save(post);
+  }
+
+  /**
+   * 取消点赞帖子
+   */
+  async unlikePost(postId: number): Promise<Post | null> {
+    const post = await this.postRepository.findOneBy({
+      id: postId,
+      isDeleted: DeleteStatus.NOT_DELETED
+    });
+
+    if (!post) {
+      throw new HTTPException(404, { message: '帖子不存在' });
+    }
+
+    if (post.likesCount > 0) {
+      post.likesCount -= 1;
+      return this.postRepository.save(post);
+    }
+
+    return post;
+  }
+}

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

@@ -29,6 +29,15 @@ export class UserEntity {
   @Column({ name: 'avatar', type: 'varchar', length: 255, nullable: true, comment: '头像' })
   avatar!: string | null;
 
+  @Column({ name: 'bio', type: 'text', nullable: true, comment: '个人简介' })
+  bio!: string | null;
+
+  @Column({ name: 'location', type: 'varchar', length: 255, nullable: true, comment: '位置' })
+  location!: string | null;
+
+  @Column({ name: 'website', type: 'varchar', length: 255, nullable: true, comment: '个人网站' })
+  website!: string | null;
+
   @Column({ name: 'is_disabled', type: 'int', default: DisabledStatus.ENABLED, comment: '是否禁用(0:启用,1:禁用)' })
   isDisabled!: DisabledStatus;
 
@@ -80,6 +89,18 @@ export const UserSchema = z.object({
     example: 'https://example.com/avatar.jpg',
     description: '用户头像'
   }),
+  bio: z.string().nullable().openapi({
+    example: '热爱技术的开发者',
+    description: '个人简介'
+  }),
+  location: z.string().max(255).nullable().openapi({
+    example: '北京',
+    description: '位置'
+  }),
+  website: z.string().url().nullable().openapi({
+    example: 'https://example.com',
+    description: '个人网站'
+  }),
   isDisabled: z.number().int().min(0).max(1).default(DisabledStatus.ENABLED).openapi({
     example: DisabledStatus.ENABLED,
     description: '是否禁用(0:启用,1:禁用)'