diff --git a/backend/app/Controller/api/v1/UserController.php b/backend/app/Controller/api/v1/UserController.php index a5a32b6..2305f08 100644 --- a/backend/app/Controller/api/v1/UserController.php +++ b/backend/app/Controller/api/v1/UserController.php @@ -66,6 +66,110 @@ class UserController extends AbstractController ]; } + /** + * 创建用户 + */ + #[RequestMapping(path: "", methods: "POST")] + #[Middleware(AuthMiddleware::class)] + public function store(): \Psr\Http\Message\ResponseInterface|array + { + $username = $this->request->input('username'); + $password = $this->request->input('password'); + $email = $this->request->input('email'); + $status_input = $this->request->input('status'); + + if (!is_string($username) || trim($username) === '') { + return $this->response->json([ + 'code' => 400, + 'message' => '用户名不能为空', + ])->withStatus(400); + } + + $username = trim($username); + $username_length = strlen($username); + if ($username_length < 3 || $username_length > 20) { + return $this->response->json([ + 'code' => 400, + 'message' => '用户名长度需在 3-20 个字符', + ])->withStatus(400); + } + + if (!is_string($password) || $password === '') { + return $this->response->json([ + 'code' => 400, + 'message' => '密码不能为空', + ])->withStatus(400); + } + + $password_length = strlen($password); + if ($password_length < 6 || $password_length > 32) { + return $this->response->json([ + 'code' => 400, + 'message' => '密码长度需在 6-32 个字符', + ])->withStatus(400); + } + + if (!is_string($email) || trim($email) === '') { + return $this->response->json([ + 'code' => 400, + 'message' => '邮箱不能为空', + ])->withStatus(400); + } + + $email = trim($email); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + return $this->response->json([ + 'code' => 400, + 'message' => '邮箱格式不正确', + ])->withStatus(400); + } + + if (strlen($email) > 100) { + return $this->response->json([ + 'code' => 400, + 'message' => '邮箱长度不能超过 100 个字符', + ])->withStatus(400); + } + + if ($status_input === null || $status_input === '') { + $status = 1; + } elseif (!is_numeric($status_input) || !in_array((int) $status_input, [0, 1], true)) { + return $this->response->json([ + 'code' => 400, + 'message' => 'status 参数必须为 0 或 1', + ])->withStatus(400); + } else { + $status = (int) $status_input; + } + + if (User::query()->where('username', $username)->exists()) { + return $this->response->json([ + 'code' => 400, + 'message' => '用户名已存在', + ])->withStatus(400); + } + + if (User::query()->where('email', $email)->exists()) { + return $this->response->json([ + 'code' => 400, + 'message' => '邮箱已被注册', + ])->withStatus(400); + } + + $user = User::query()->create([ + 'username' => $username, + 'password' => $password, + 'email' => $email, + 'status' => $status, + ]); + + return [ + 'code' => 0, + 'message' => '创建成功', + 'data' => $user, + ]; + } + /** * 用户详情 * @@ -90,4 +194,167 @@ class UserController extends AbstractController 'data' => $user, ]; } + + /** + * 更新用户信息 + * + * @param int $id 用户 ID + */ + #[RequestMapping(path: "{id}", methods: "PUT")] + #[Middleware(AuthMiddleware::class)] + public function update(int $id): \Psr\Http\Message\ResponseInterface|array + { + $user = User::query()->find($id); + + if (!$user) { + return $this->response->json([ + 'code' => 404, + 'message' => '用户不存在', + ])->withStatus(404); + } + + if ($this->request->input('password') !== null) { + return $this->response->json([ + 'code' => 400, + 'message' => '更新接口不支持修改密码', + ])->withStatus(400); + } + + $username = $this->request->input('username'); + $email = $this->request->input('email'); + $ext = $this->request->input('ext'); + $updates = []; + + if ($username !== null) { + if (!is_string($username) || trim($username) === '') { + return $this->response->json([ + 'code' => 400, + 'message' => '用户名不能为空', + ])->withStatus(400); + } + + $username = trim($username); + $username_length = strlen($username); + if ($username_length < 3 || $username_length > 20) { + return $this->response->json([ + 'code' => 400, + 'message' => '用户名长度需在 3-20 个字符', + ])->withStatus(400); + } + + if (User::query()->where('username', $username)->where('id', '!=', $user->id)->exists()) { + return $this->response->json([ + 'code' => 400, + 'message' => '用户名已存在', + ])->withStatus(400); + } + + $updates['username'] = $username; + } + + if ($email !== null) { + if (!is_string($email) || trim($email) === '') { + return $this->response->json([ + 'code' => 400, + 'message' => '邮箱不能为空', + ])->withStatus(400); + } + + $email = trim($email); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + return $this->response->json([ + 'code' => 400, + 'message' => '邮箱格式不正确', + ])->withStatus(400); + } + + if (strlen($email) > 100) { + return $this->response->json([ + 'code' => 400, + 'message' => '邮箱长度不能超过 100 个字符', + ])->withStatus(400); + } + + if (User::query()->where('email', $email)->where('id', '!=', $user->id)->exists()) { + return $this->response->json([ + 'code' => 400, + 'message' => '邮箱已被注册', + ])->withStatus(400); + } + + $updates['email'] = $email; + } + + if ($ext !== null) { + if (!is_array($ext)) { + return $this->response->json([ + 'code' => 400, + 'message' => 'ext 必须为对象', + ])->withStatus(400); + } + + $updates['ext'] = $ext; + } + + if ($updates === []) { + return $this->response->json([ + 'code' => 400, + 'message' => '缺少可更新字段', + ])->withStatus(400); + } + + $user->fill($updates); + $user->save(); + $user->refresh(); + + return [ + 'code' => 0, + 'message' => '更新成功', + 'data' => $user, + ]; + } + + /** + * 更新用户状态 + * + * @param int $id 用户 ID + */ + #[RequestMapping(path: "{id}/status", methods: "PATCH")] + #[Middleware(AuthMiddleware::class)] + public function updateStatus(int $id): \Psr\Http\Message\ResponseInterface|array + { + $user = User::query()->find($id); + + if (!$user) { + return $this->response->json([ + 'code' => 404, + 'message' => '用户不存在', + ])->withStatus(404); + } + + $status_input = $this->request->input('status'); + if ($status_input === null || $status_input === '') { + return $this->response->json([ + 'code' => 400, + 'message' => '缺少 status 参数', + ])->withStatus(400); + } + + if (!is_numeric($status_input) || !in_array((int) $status_input, [0, 1], true)) { + return $this->response->json([ + 'code' => 400, + 'message' => 'status 参数必须为 0 或 1', + ])->withStatus(400); + } + + $user->status = (int) $status_input; + $user->save(); + $user->refresh(); + + return [ + 'code' => 0, + 'message' => '状态更新成功', + 'data' => $user, + ]; + } } diff --git a/backend/test/Cases/Integration/User/UserControllerTest.php b/backend/test/Cases/Integration/User/UserControllerTest.php index 9c35bf8..7ec685d 100644 --- a/backend/test/Cases/Integration/User/UserControllerTest.php +++ b/backend/test/Cases/Integration/User/UserControllerTest.php @@ -65,6 +65,25 @@ class UserControllerTest extends TestCase return $user; } + protected function makeUserPayload(array $overrides = []): array + { + $suffix = bin2hex(random_bytes(4)); + + return array_merge([ + 'username' => 'user_' . $suffix, + 'password' => 'Pass_' . $suffix, + 'email' => 'user_' . $suffix . '@example.com', + 'status' => 1, + ], $overrides); + } + + protected function createUser(array $overrides = []): User + { + $payload = $this->makeUserPayload($overrides); + + return User::query()->create($payload); + } + // ========== 列表接口测试 ========== public function test_list_users_returns_paginated_data(): void @@ -206,6 +225,148 @@ class UserControllerTest extends TestCase $response->assertJsonPath('code', 404); } + // ========== 创建接口测试 ========== + + public function test_create_user_success(): void + { + $payload = $this->makeUserPayload(); + + $response = $this->post('/api/v1/users', $payload, $this->authHeaders()); + + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + $response->assertJsonPath('data.username', $payload['username']); + $response->assertJsonPath('data.email', $payload['email']); + $response->assertJsonPath('data.status', 1); + } + + public function test_create_user_validation_error_returns_400(): void + { + $payload = $this->makeUserPayload([ + 'username' => 'ab', + ]); + + $response = $this->post('/api/v1/users', $payload, $this->authHeaders()); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_create_user_duplicate_username_returns_400(): void + { + $user = $this->createUser(); + + $payload = $this->makeUserPayload([ + 'username' => $user->username, + ]); + + $response = $this->post('/api/v1/users', $payload, $this->authHeaders()); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_create_user_duplicate_email_returns_400(): void + { + $user = $this->createUser(); + + $payload = $this->makeUserPayload([ + 'email' => $user->email, + ]); + + $response = $this->post('/api/v1/users', $payload, $this->authHeaders()); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + // ========== 更新接口测试 ========== + + public function test_update_user_partial_fields(): void + { + $user = $this->createUser(); + $suffix = bin2hex(random_bytes(4)); + $updated_email = 'updated_' . $suffix . '@example.com'; + $ext = ['nickname' => 'Tester']; + + $response = $this->put('/api/v1/users/' . $user->id, [ + 'email' => $updated_email, + 'ext' => $ext, + ], $this->authHeaders()); + + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + $response->assertJsonPath('data.email', $updated_email); + $response->assertJsonPath('data.ext.nickname', 'Tester'); + + $user->refresh(); + $this->assertSame($updated_email, $user->email); + $this->assertSame($ext, $user->ext); + } + + public function test_update_user_unique_conflict_returns_400(): void + { + $existing = $this->createUser(); + $target = $this->createUser(); + + $response = $this->put('/api/v1/users/' . $target->id, [ + 'username' => $existing->username, + ], $this->authHeaders()); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_update_user_not_found_returns_404(): void + { + $response = $this->put('/api/v1/users/999999', [ + 'email' => 'missing@example.com', + ], $this->authHeaders()); + + $response->assertStatus(404); + $response->assertJsonPath('code', 404); + } + + // ========== 状态切换接口测试 ========== + + public function test_patch_user_status_updates_status(): void + { + $user = $this->createUser(); + + $response = $this->patch('/api/v1/users/' . $user->id . '/status', [ + 'status' => 0, + ], $this->authHeaders()); + + $response->assertStatus(200); + $response->assertJsonPath('code', 0); + $response->assertJsonPath('data.status', 0); + + $user->refresh(); + $this->assertSame(0, $user->status); + } + + public function test_patch_user_status_invalid_returns_400(): void + { + $user = $this->createUser(); + + $response = $this->patch('/api/v1/users/' . $user->id . '/status', [ + 'status' => 2, + ], $this->authHeaders()); + + $response->assertStatus(400); + $response->assertJsonPath('code', 400); + } + + public function test_patch_user_status_not_found_returns_404(): void + { + $response = $this->patch('/api/v1/users/999999/status', [ + 'status' => 0, + ], $this->authHeaders()); + + $response->assertStatus(404); + $response->assertJsonPath('code', 404); + } + // ========== 认证拦截测试 ========== public function test_list_users_without_token_returns_401(): void