Compare commits

...

29 Commits

Author SHA1 Message Date
dayjoy
d474216047 fix: 增加bug修复描述 2025-10-15 14:39:19 +08:00
dayjoy
71a7d7ba8c fix: 页面上不要展示主题切换按钮 2025-10-14 15:11:11 +08:00
dayjoy
bfaa656406 fix: 禁止出现横向滚动条 2025-10-14 15:09:35 +08:00
dayjoy
b293b91697 fix: Adjust font size for inputs on iOS devices 2025-10-14 14:47:12 +08:00
dayjoy
5495c7bf25 fix: 更新viewport meta,禁止缩放 2025-10-14 14:46:16 +08:00
dayjoy
2f38d95e8c fix: example改为字段描述和example 2025-10-13 14:13:37 +08:00
dayjoy
defe5ac49b fix: user的createdAt还是补充上 2025-10-13 10:47:57 +08:00
dayjoy
97bd5d013b feat: 每个接口都需要写 @Example 装饰器 2025-10-11 18:10:40 +08:00
dayjoy
1659d03040 feat: 补充接口example 2025-10-11 18:02:08 +08:00
dayjoy
99c8fa5f2d feat: 明确OperationId 2025-10-11 14:20:29 +08:00
dayjoy
7f057a50ca feat: 提示词更新:尽量不出现横向滚动条 2025-10-11 11:40:33 +08:00
dayjoy
b19b9f8b5f feat: 添加过滤内部接口的脚本 2025-10-11 11:07:18 +08:00
dayjoy
339dccdaa5 feat: 添加/api/swagger/swagger.json 2025-10-11 10:50:28 +08:00
dayjoy
c6464fe1d1 feat: 强调不许修改client相对路径 2025-10-10 16:45:02 +08:00
dayjoy
2341e36aa9 chore(comment): 注释改为英文 2025-09-30 14:27:53 +08:00
dayjoy
2651bd1373 feat(prompt): 确保页面背景色、文字色、按钮色等UI元素在不同主题下均有良好对比度和可读性 2025-09-30 14:23:22 +08:00
dayjoy
eb6eceffe9 feat(theme): 支持读取主题query参数 2025-09-30 14:23:04 +08:00
dayjoy
7c1241b26c feat(agent): 修改提示词以适应变化 2025-09-26 17:37:33 +08:00
dayjoy
718a535cb1 fix(client): rename getGroupMembers 2025-09-26 17:32:19 +08:00
dayjoy
eb39f41f0c fix(server): 处理错误消息 2025-09-26 17:28:06 +08:00
dayjoy
a4ccdada1c fix(script): 修改server启动命令 2025-09-26 17:25:58 +08:00
dayjoy
d8a921b50a fix(server): 处理错误消息 2025-09-26 17:25:43 +08:00
dayjoy
a5048bafb3 fix(client): 获取token逻辑修正 2025-09-26 17:10:17 +08:00
dayjoy
999a8e2520 fix(client): 接口结果修正 2025-09-26 16:57:09 +08:00
dayjoy
b78a07f192 fix(client): 替换接口path 2025-09-26 16:46:38 +08:00
dayjoy
5688bb4242 fix(server): 接口path修改 2025-09-26 16:45:12 +08:00
dayjoy
b1530ce06e fix(server): 用户信息接口不返回token 2025-09-26 16:29:26 +08:00
dayjoy
f53aed1b82 fix(script): 修改server启动命令 2025-09-26 16:29:09 +08:00
dayjoy
8c60050ff1 fix(server): 把/v3/api-docs加回来 2025-09-26 16:10:53 +08:00
19 changed files with 279 additions and 97 deletions

View File

@@ -14,16 +14,31 @@
- 基于响应式设计的单页应用SPA - 基于响应式设计的单页应用SPA
- 专为移动端优化,**采用单一入口点架构**,所有功能模块通过组件化方式在同一页面内动态加载和切换,确保用户体验的连贯性和加载性能的优化 - 专为移动端优化,**采用单一入口点架构**,所有功能模块通过组件化方式在同一页面内动态加载和切换,确保用户体验的连贯性和加载性能的优化
- **主题适配**: 确保组件支持亮色/暗色主题 - **主题适配**: 确保组件支持亮色/暗色主题
- 确保页面背景色、文字色、按钮色等UI元素在不同主题下均有良好对比度和可读性
- 但是页面上不要展示主题切换按钮主题切换根据url参数 `?theme=dark``?theme=light` 来控制
- **错误展示**前端页面需要有统一的错误展示用于显示API请求失败或其他操作错误的信息 - **错误展示**前端页面需要有统一的错误展示用于显示API请求失败或其他操作错误的信息
- 不需要实现登录页,默认访问应用的用户都是已登录状态 - 不需要实现登录页,默认访问应用的用户都是已登录状态
- 所有请求统一使用 `/src/api/index` 中的 `api` 方法进行调用,因为已经内置了必要的请求头封装 - 所有请求统一使用 `/src/api/index` 中的 `api` 方法进行调用,因为已经内置了必要的请求头封装
- 如果有需要,使用 `/src/api/user` 中的 `getUserInfo` 方法获取当前用户信息 - 如果有需要,使用 `/src/api/user` 中的 `getUserInfo` 方法获取当前用户信息
- 如果有需要,使用 `/src/api/user` 中的 `getGroupUsers` 方法获取全部用户信息列表 - 如果有需要,使用 `/src/api/user` 中的 `getGroupMembers` 方法获取全部用户信息列表
### 2. 后端实现策略 ### 2. 后端实现策略
- 所有接口以 `/api` 为前缀 - 所有接口以 `/api` 为前缀
- 已经为项目封装了当前用户信息获取的逻辑,提供了中间件,`userInfoMiddleware`,见: `src/middleware/auth.ts` - 所有接口按照 `tsoa` 规范来编写,参考 `src/controllers` 目录下的 `UserController``GroupController` 示例
- 已经为项目封装了当前群组所有用户信息列表的获取逻辑,提供了中间件,`groupInfoMiddleware`,见: `src/middleware/auth.ts` - 如果接口需要登录,请使用 `@Security('jwt')` 装饰器tsoa 会自动调用登录校验,并把 user 信息写到req上参考 `UserController` 示例
- **每个接口相关的类型定义(如interface、type)等都需要按照标准注释进行类型及字段的描述注释。不明确的字段如string类型的日期等还需要加上 @example 表明字段具体的格式示例**,例如:
```ts
/**
* 用户ID
* @example "123456"
*/
id: string;
/**
* 注册日期格式为yyyy-MM-dd
* @example "2025-01-01"
*/
registryDate: string;
```
### 3. 开发规范 ### 3. 开发规范
- **文件命名**: 使用kebab-case命名文件和文件夹 - **文件命名**: 使用kebab-case命名文件和文件夹
@@ -32,6 +47,11 @@
- **类型定义**: 为所有数据结构定义TypeScript类型 - **类型定义**: 为所有数据结构定义TypeScript类型
- **代码组织**: 保持清晰的目录结构和模块化设计 - **代码组织**: 保持清晰的目录结构和模块化设计
### 4. 设计规范
- **响应式**: 确保所有页面在不同屏幕尺寸下均有良好展示效果
- **滚动条**: 根据屏幕宽度变化自适应,🈲禁止出现横向滚动条
## 任务执行流程 ## 任务执行流程
1. **需求分析**: 仔细解析JSON需求识别核心功能点 1. **需求分析**: 仔细解析JSON需求识别核心功能点
@@ -50,6 +70,8 @@
用户每次修改后,请在现有代码基础上**针对JSON中较上一次有变动的地方重点编辑实现**,确保所有变更都被正确反映。 用户每次修改后,请在现有代码基础上**针对JSON中较上一次有变动的地方重点编辑实现**,确保所有变更都被正确反映。
此外用户还可能通过 `appBug` 字段反馈bug**如果 `appBug` 字段不为空字符串则必须优先修复对应的bug**并确保修复后代码依然符合用户最新的JSON需求。
## 注意事项 ## 注意事项
- 严格遵循现有的项目结构和编码规范 - 严格遵循现有的项目结构和编码规范
@@ -62,7 +84,9 @@
请基于以上规范和用户JSON需求生成完整的、可运行的、符合输出格式要求的代码实现。 请基于以上规范和用户JSON需求生成完整的、可运行的、符合输出格式要求的代码实现。
接下来我会输出用户JSON需求其中前端需求放在 `webapp_requirements`, 后端需求放在 `node_backend_requirements` 接下来我会输出用户JSON需求其中前端需求放在 `webapp_requirements`, 后端需求放在 `node_backend_requirements`bug修复需求放在 `appBug`。
> 如果 webapp_requirements 中 is_needed = false代表不需要前端,只需要生成后端代码 > 如果 webapp_requirements 为 null代表不需要前端web页面或者web页面本次不需要变更,只需要生成后端代码
> 如果 node_backend_requirements 为 null代表后端代码本次不需要变更
> 如果 appBug 不为空字符串,代表本次需要修复 bug

View File

@@ -6,9 +6,9 @@
], ],
"scripts": { "scripts": {
"start": "yarn start:all", "start": "yarn start:all",
"start:all": "concurrently -n 'client,server' 'yarn workspace egret-app-client dev' 'yarn workspace egret-app-server start'", "start:all": "concurrently -n 'client,server' 'yarn workspace egret-app-client dev' 'yarn workspace egret-app-server serve'",
"start:client": "yarn workspace egret-app-client dev", "start:client": "yarn workspace egret-app-client dev",
"start:server": "yarn workspace egret-app-server start", "start:server": "yarn workspace egret-app-server serve",
"build": "lerna run build && node ./scripts/post-build.js" "build": "lerna run build && node ./scripts/post-build.js"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/great-egret-192.png" /> <link rel="icon" type="image/svg+xml" href="/great-egret-192.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<title>Egret App</title> <title>Egret App</title>
</head> </head>
<body> <body>

View File

@@ -2,15 +2,30 @@ import "./App.css";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Routes } from "react-router-dom"; import { Routes } from "react-router-dom";
import {handleAuthTokenAndGroupIdFromUrl} from "./utils/auth"; import {handleAuthTokenAndGroupIdFromUrl} from "./utils/auth";
import { useTheme } from "@/components/theme-provider";
const App: React.FC = () => { const App: React.FC = () => {
const { setTheme } = useTheme();
useEffect(() => { useEffect(() => {
// 在组件挂载时处理URL中的authToken groupId // Consume authToken & groupId from URL and persist
handleAuthTokenAndGroupIdFromUrl(); handleAuthTokenAndGroupIdFromUrl();
}, []);
// Read optional theme ('light' | 'dark') from URL and persist via ThemeProvider
const params = new URLSearchParams(window.location.search);
const themeParam = params.get('theme');
if (themeParam === 'light' || themeParam === 'dark') {
setTheme(themeParam);
// Clean up URL param after applying
const url = new URL(window.location.href);
url.searchParams.delete('theme');
window.history.replaceState(null, '', url.toString());
}
}, [setTheme]);
return ( return (
<div className="w-full h-full bg-gray-50"> <div className="w-full min-h-dvh bg-background text-foreground">
<main> <main>
<Routes></Routes> <Routes></Routes>
</main> </main>

View File

@@ -5,8 +5,7 @@ interface RequestOptions extends RequestInit {
} }
/** /**
* 统一的fetch请求封装 * Unified fetch wrapper that adds auth and group headers
* 自动添加Authorization头部
*/ */
export const apiRequest = async <T = any>( export const apiRequest = async <T = any>(
endpoint: string, endpoint: string,
@@ -17,34 +16,34 @@ export const apiRequest = async <T = any>(
const url = endpoint; const url = endpoint;
// 准备请求头 // Prepare request headers
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options.headers, ...options.headers,
}; };
// 如果有authToken添加Authorization头 // Attach Authorization header if token exists
if (authToken) { if (authToken) {
headers.authorization = `Bearer ${authToken}`; headers.authorization = `Bearer ${authToken}`;
} }
// 如果有groupId添加groupId头 // Attach groupId header if present
if (groupId) { if (groupId) {
headers.groupId = groupId; headers.groupId = groupId;
} }
// 发起请求 // Fire request
const response = await fetch(url, { const response = await fetch(url, {
...options, ...options,
headers, headers,
}); });
// 处理响应 // Handle non-2xx
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
// 尝试解析JSON如果失败则返回响应对象 // Try to parse JSON; fallback to raw response
try { try {
return await response.json(); return await response.json();
} catch { } catch {
@@ -52,34 +51,40 @@ export const apiRequest = async <T = any>(
} }
}; };
// 便捷的HTTP方法封装 // Convenience HTTP helpers
export const api = { export const api = {
get: <T = any>(endpoint: string, options?: RequestOptions): Promise<T> => get: <T = any>(endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>> =>
apiRequest<T>(endpoint, { ...options, method: 'GET' }), apiRequest<ApiResponse<T>>(endpoint, { ...options, method: 'GET' }),
post: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> => post: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<ApiResponse<T>> =>
apiRequest<T>(endpoint, { apiRequest<ApiResponse<T>>(endpoint, {
...options, ...options,
method: 'POST', method: 'POST',
body: data ? JSON.stringify(data) : undefined, body: data ? JSON.stringify(data) : undefined,
}), }),
put: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> => put: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<ApiResponse<T>> =>
apiRequest<T>(endpoint, { apiRequest<ApiResponse<T>>(endpoint, {
...options, ...options,
method: 'PUT', method: 'PUT',
body: data ? JSON.stringify(data) : undefined, body: data ? JSON.stringify(data) : undefined,
}), }),
patch: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> => patch: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<ApiResponse<T>> =>
apiRequest<T>(endpoint, { apiRequest<ApiResponse<T>>(endpoint, {
...options, ...options,
method: 'PATCH', method: 'PATCH',
body: data ? JSON.stringify(data) : undefined, body: data ? JSON.stringify(data) : undefined,
}), }),
delete: <T = any>(endpoint: string, options?: RequestOptions): Promise<T> => delete: <T = any>(endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>> =>
apiRequest<T>(endpoint, { ...options, method: 'DELETE' }), apiRequest<ApiResponse<T>>(endpoint, { ...options, method: 'DELETE' }),
}; };
export type ApiResponse<T> = {
code: number;
message: string;
data: T;
}
export default api; export default api;

View File

@@ -1,5 +1,4 @@
import api from "@/api/index.ts"; import api from "@/api/index.ts";
import {getGroupId} from "@/utils/auth.ts";
export type UserInfo = { export type UserInfo = {
userId: number; userId: number;
@@ -13,17 +12,17 @@ export type UserInfo = {
} }
/** /**
* 获取当前用户的信息 * Get current user information
*/ */
export const getUserInfo = async (): Promise<UserInfo> => { export const getUserInfo = async (): Promise<UserInfo | null> => {
return await api.post<UserInfo>('https://egret.byteawake.com/api/user/info'); const res = await api.get<UserInfo>('/api/user/info');
return res.code === 200 ? res.data : null;
}; };
/** /**
* 获取群组内所有用户的信息 * Get all users in the current group
*/ */
export const getGroupUsers = async (): Promise<UserInfo[]> => { export const getGroupMembers = async (): Promise<UserInfo[]> => {
return await api.post<UserInfo[]>('https://egret.byteawake.com/api/group/members', { const res = await api.get<UserInfo[]>('/api/group/members');
groupId: Number(getGroupId()), return res.code === 200 ? res.data || [] : [];
});
}; };

View File

@@ -130,3 +130,18 @@
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
@media only screen and (max-device-width: 414px) and (orientation: portrait) and (-webkit-min-device-pixel-ratio: 2) {
/* Adjust font size for inputs on iOS devices */
.flex-input {
font-size: 16px !important;
}
}
@media screen and (-webkit-min-device-pixel-ratio:2) {
select,
textarea,
input {
font-size: 16px !important;
}
}

View File

@@ -1,17 +1,17 @@
// 处理认证相关的工具函数 // Auth utilities
const AUTH_TOKEN_KEY = 'authToken'; const AUTH_TOKEN_KEY = 'authToken';
const GROUP_ID_KEY = 'groupId'; const GROUP_ID_KEY = 'groupId';
/** /**
* 从URL搜索参数中获取authToken * Get authToken from URL search params
*/ */
export const getAuthTokenFromUrl = (): string | null => { export const getAuthTokenFromUrl = (): string | null => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('authToken'); return urlParams.get('authToken');
}; };
/** /**
* 从URL搜索参数中获取groupId * Get groupId from URL search params
*/ */
export const getGroupIdFromUrl = (): string | null => { export const getGroupIdFromUrl = (): string | null => {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@@ -19,50 +19,51 @@ export const getGroupIdFromUrl = (): string | null => {
}; };
/** /**
* 保存authTokenlocalStorage * Save authToken to localStorage
*/ */
export const saveAuthToken = (token: string): void => { export const saveAuthToken = (token: string): void => {
localStorage.setItem(AUTH_TOKEN_KEY, token); localStorage.setItem(AUTH_TOKEN_KEY, token);
}; };
/** /**
* 保存groupIdlocalStorage * Save groupId to localStorage
*/ */
export const saveGroupId = (groupId: string): void => { export const saveGroupId = (groupId: string): void => {
localStorage.setItem(GROUP_ID_KEY, groupId); localStorage.setItem(GROUP_ID_KEY, groupId);
}; };
/** /**
* 从localStorage获取authToken * Get authToken with URL fallback
*/ */
export const getAuthToken = (): string | null => { export const getAuthToken = (): string | null => {
return localStorage.getItem(AUTH_TOKEN_KEY); const tokenFromUrl = getAuthTokenFromUrl();
return tokenFromUrl || localStorage.getItem(AUTH_TOKEN_KEY);
}; };
/** /**
* 从localStorage获取groupId * Get groupId with URL fallback
*/ */
export const getGroupId = (): string | null => { export const getGroupId = (): string | null => {
return localStorage.getItem(GROUP_ID_KEY); const groupIdFromUrl = getGroupIdFromUrl();
return groupIdFromUrl || localStorage.getItem(GROUP_ID_KEY);
}; };
/** /**
* 清除authToken * Clear authToken
*/ */
export const clearAuthToken = (): void => { export const clearAuthToken = (): void => {
localStorage.removeItem(AUTH_TOKEN_KEY); localStorage.removeItem(AUTH_TOKEN_KEY);
}; };
/** /**
* 清除groupId * Clear groupId
*/ */
export const clearGroupId = (): void => { export const clearGroupId = (): void => {
localStorage.removeItem(GROUP_ID_KEY); localStorage.removeItem(GROUP_ID_KEY);
}; };
/** /**
* 检查并处理URL中的authToken * Consume authToken/groupId from URL and persist, then clean URL
* 如果URL中有authToken参数则保存到localStorage并从URL中移除
*/ */
export const handleAuthTokenAndGroupIdFromUrl = (): void => { export const handleAuthTokenAndGroupIdFromUrl = (): void => {
const tokenFromUrl = getAuthTokenFromUrl(); const tokenFromUrl = getAuthTokenFromUrl();
@@ -78,7 +79,7 @@ export const handleAuthTokenAndGroupIdFromUrl = (): void => {
updated = true; updated = true;
} }
// Clean URL if we consumed any param // Clean URL if any param was consumed
if (updated) { if (updated) {
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.delete('authToken'); url.searchParams.delete('authToken');

View File

@@ -4,8 +4,8 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "tsoa -c tsoaConfig.json spec-and-routes && tsc && tsc-alias", "build": "tsoa -c tsoaConfig.json spec-and-routes && node scripts/clean-swagger.js && tsc && tsc-alias",
"serve": "node ./build/index.js", "serve": "yarn build && node ./build/index.js",
"start": "nodemon" "start": "nodemon"
}, },
"keywords": [], "keywords": [],

View File

@@ -0,0 +1,71 @@
import fs from "fs";
const filePath = "./build/swagger.json";
const swagger = JSON.parse(fs.readFileSync(filePath, "utf8"));
// ==============================
// ① 删除不希望公开的接口路径
// ==============================
const pathsToRemove = ["/api/test", "/api/user/info", "/api/group/members"];
for (const path of pathsToRemove) {
if (swagger.paths?.[path]) {
delete swagger.paths[path];
console.log(`🧹 Removed path: ${path}`);
}
}
// ==============================
// ② 收集引用的 schema 名称(递归)
// ==============================
const referenced = new Set();
function collectRefs(obj) {
if (!obj || typeof obj !== "object") return;
for (const key in obj) {
const value = obj[key];
if (key === "$ref" && typeof value === "string" && value.startsWith("#/components/schemas/")) {
const name = value.split("/").pop();
if (!referenced.has(name)) {
referenced.add(name);
// 递归收集该 schema 内的引用
const schema = swagger.components?.schemas?.[name];
if (schema) collectRefs(schema);
}
} else {
collectRefs(value);
}
}
}
// 先从 paths 开始收集
collectRefs(swagger.paths);
// 然后递归收集 schemas 内部互相引用的部分
let prevCount = 0;
do {
prevCount = referenced.size;
for (const name of Array.from(referenced)) {
const schema = swagger.components?.schemas?.[name];
if (schema) collectRefs(schema);
}
} while (referenced.size > prevCount);
// ==============================
// ③ 删除未被引用的 schema
// ==============================
if (swagger.components?.schemas) {
const allSchemas = Object.keys(swagger.components.schemas);
const unused = allSchemas.filter((name) => !referenced.has(name));
for (const name of unused) {
delete swagger.components.schemas[name];
}
console.log(`🧩 Removed ${unused.length} unused schemas`);
}
// ==============================
// ④ 保存结果
// ==============================
fs.writeFileSync(filePath, JSON.stringify(swagger, null, 2));
console.log("✅ Swagger cleanup complete!");

View File

@@ -1,10 +1,10 @@
import {Controller, Get, Route, Response, Tags, Request, Security} from 'tsoa'; import {Controller, Get, Route, Response, Tags, Request, Security, OperationId} from 'tsoa';
import {ApiError, ApiResponse} from '../types/api'; import {ApiError, ApiResponse} from '../types/api';
import type {Request as ExpressRequest} from 'express'; import type {Request as ExpressRequest} from 'express';
import axios from "axios"; import axios from "axios";
import type {UserInfo} from "../types/user"; import type {UserInfo} from "../types/user";
export const getGroupUsers = async (groupId: number, token: string): Promise<UserInfo[]> => { export const getGroupMembers = async (groupId: number, token: string): Promise<UserInfo[]> => {
try { try {
const response = await axios.post<ApiResponse<UserInfo[]>>( const response = await axios.post<ApiResponse<UserInfo[]>>(
"https://egret.byteawake.com/api/group/members", "https://egret.byteawake.com/api/group/members",
@@ -18,20 +18,20 @@ export const getGroupUsers = async (groupId: number, token: string): Promise<Use
); );
if (response.data.code !== 200) { if (response.data.code !== 200) {
throw new Error(`Failed to get group users: ${response.data.message}`); throw new Error(response.data.message);
} }
return response.data.data ?? []; return response.data.data ?? [];
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {
// API returned error response // API returned error response
throw new Error(`Failed to get user information: ${error.response.status} ${error.response.statusText}`); throw new ApiError(400, `Failed to get group members information: ${error.response.status} ${error.response.statusText}`);
} else if (error.request) { } else if (error.request) {
// Request was sent but no response received // Request was sent but no response received
throw new Error("Failed to get user information: timeout or network error"); throw new ApiError(400, "Failed to get group members information: timeout or network error");
} else { } else {
// Other errors // Other errors
throw new Error(`Failed to get user information: ${error.message}`); throw new ApiError(400, `Failed to get group members information: ${error.message}`);
} }
} }
}; };
@@ -40,16 +40,17 @@ export const getGroupUsers = async (groupId: number, token: string): Promise<Use
@Tags('Chat Group') @Tags('Chat Group')
export class GroupController extends Controller { export class GroupController extends Controller {
/** /**
* 获取当前聊天群组的全部用户信息 * Get all users in the current chat group
* @summary 获取当前聊天群组的全部用户信息 * @summary Get group members
* @description 获取当前聊天群组的全部用户信息 * @description Returns the full member list of the current chat group
*/ */
@Get('/users') @Get('/members')
@OperationId('Group_GetGroupMembers') // MUST: Specify operationId for better API documentation
@Security("jwt") @Security("jwt")
@Response<ApiResponse<UserInfo[]>>(200, 'Success') @Response<ApiResponse<UserInfo[]>>(200, 'Success')
@Response<ApiResponse<null>>(400, 'Bad Request') @Response<ApiResponse<null>>(400, 'Bad Request')
@Response<ApiResponse<null>>(401, 'Unauthorized') @Response<ApiResponse<null>>(401, 'Unauthorized')
public async getGroupUsers(@Request() req: ExpressRequest): Promise<ApiResponse<UserInfo[]>> { public async getGroupMembers(@Request() req: ExpressRequest): Promise<ApiResponse<UserInfo[]>> {
const user = req.user; const user = req.user;
const groupId = req.headers.groupid; const groupId = req.headers.groupid;
@@ -57,7 +58,7 @@ export class GroupController extends Controller {
throw new ApiError(400, 'groupId is required in headers'); throw new ApiError(400, 'groupId is required in headers');
} }
const users = await getGroupUsers(Number(groupId), user.token); const users = await getGroupMembers(Number(groupId), user.token);
return { return {
code: 200, code: 200,
@@ -65,4 +66,4 @@ export class GroupController extends Controller {
data: users, data: users,
}; };
} }
} }

View File

@@ -1,15 +1,16 @@
import { Controller, Get, Route, Response, Tags } from 'tsoa'; import {Controller, Get, Route, Response, Tags, OperationId} from 'tsoa';
import type { ApiResponse } from '../types/api'; import type { ApiResponse } from '../types/api';
@Route('api') @Route('api')
@Tags('Test') @Tags('Test')
export class TestController extends Controller { export class TestController extends Controller {
/** /**
* 测试接口 * Test endpoint
* @summary 测试接口 * @summary Test endpoint
* @description 用于测试API连通性的基础接口 * @description Basic endpoint to verify API connectivity
*/ */
@Get('/test') @Get('/test')
@OperationId('Test_GetTest') // MUST: Specify operationId for better API documentation
@Response<ApiResponse>(200, 'Success') @Response<ApiResponse>(200, 'Success')
public async getTest(): Promise<ApiResponse> { public async getTest(): Promise<ApiResponse> {
return { return {

View File

@@ -1,4 +1,4 @@
import {Controller, Get, Route, Response, Tags, Middlewares, Request, Security} from 'tsoa'; import {Controller, Get, Route, Response, Tags, Request, Security, OperationId} from 'tsoa';
import type { ApiResponse } from '../types/api'; import type { ApiResponse } from '../types/api';
import type { Request as ExpressRequest } from 'express'; import type { Request as ExpressRequest } from 'express';
import type {UserInfo} from "../types/user"; import type {UserInfo} from "../types/user";
@@ -8,19 +8,21 @@ import type {UserInfo} from "../types/user";
@Tags('User') @Tags('User')
export class UserController extends Controller { export class UserController extends Controller {
/** /**
* 获取当前用户的信息 * Get current user information
* @summary 获取当前用户的信息 * @summary Get current user information
* @description 获取当前用户的信息 * @description Returns the authenticated user's profile
*/ */
@Get('/info') @Get('/info')
@OperationId('User_GetUserGroupInfo') // MUST: Specify operationId for better API documentation
@Security('jwt') @Security('jwt')
@Response<ApiResponse<UserInfo>>(200, 'Success') @Response<ApiResponse<UserInfo>>(200, 'Success')
@Response(401, 'Unauthorized') @Response(401, 'Unauthorized')
public async getUserGroupInfo(@Request() req: ExpressRequest): Promise<ApiResponse<UserInfo>> { public async getUserGroupInfo(@Request() req: ExpressRequest): Promise<ApiResponse<Omit<UserInfo, 'token'>>> {
const { token, ...rest } = req.user;
return { return {
code: 200, code: 200,
message: 'success', message: 'success',
data: req.user data: rest
}; };
} }
} }

View File

@@ -10,7 +10,7 @@ dotenv.config({ path: path.resolve(__dirname, "../../../.env") });
import "./database"; import "./database";
import { sequelize } from "./database/instance"; import { sequelize } from "./database/instance";
import {errorHandler} from "./middleware/errorHandler"; import {errorHandler} from "./middleware/errorHandler";
import { RegisterRoutes } from "./routes/routes"; // tsoa 生成的 import {RegisterTsoaRoutes} from "./middleware/tsoa.middleware";
const port = process.env.PORT || 3005; const port = process.env.PORT || 3005;
@@ -20,10 +20,10 @@ app.use(nocache());
app.use(cors()); app.use(cors());
app.use(express.json({ limit: "100mb" })); app.use(express.json({ limit: "100mb" }));
app.use(compression()); app.use(compression());
app.use(express.static(path.resolve(__dirname, "client"))); app.use(express.static(path.resolve(__dirname, "client"))); // DON'T MODIFY THIS LINE, IT IS USED IN BUILD
// Register tsoa routes // Register tsoa routes
RegisterRoutes(app); RegisterTsoaRoutes(app);
app.use(errorHandler); app.use(errorHandler);

View File

@@ -16,7 +16,7 @@ export async function expressAuthentication(
throw new ApiError(401, "Unauthorized"); throw new ApiError(401, "Unauthorized");
} }
// 返回的对象会挂到 request.user // Returned object is assigned to request.user
return await getUserInfoByToken(token); return await getUserInfoByToken(token);
} }
throw new ApiError(401, "Unsupported security scheme"); throw new ApiError(401, "Unsupported security scheme");
@@ -45,7 +45,7 @@ export const getUserInfoByToken = async (token: string): Promise<UserInfo> => {
try { try {
const response = await axios.post<ApiResponse<Omit<UserInfo, 'token'>>>( const response = await axios.post<ApiResponse<Omit<UserInfo, 'token'>>>(
"https://egret.byteawake.com/api/user/info", "https://egret.byteawake.com/api/user/info",
{}, // 请求体数据,这里为空对象 {}, // Empty request body
{ {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
@@ -55,23 +55,22 @@ export const getUserInfoByToken = async (token: string): Promise<UserInfo> => {
); );
if (response.data.code !== 200 || !response.data.data) { if (response.data.code !== 200 || !response.data.data) {
throw new Error(`Failed to get user information: ${response.data.message}`); throw new Error(response.data.message);
} }
return {...response.data.data, token}; return {...response.data.data, token};
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {
// API returned error response // API returned error response
throw new Error(`Failed to get user information: ${error.response.status} ${error.response.statusText}`); throw new ApiError(401, `Failed to get user information: ${error.response.status} ${error.response.statusText}`);
} else if (error.request) { } else if (error.request) {
// Request was sent but no response received // Request was sent but no response received
throw new Error("Failed to get user information: timeout or network error"); throw new ApiError(401, "Failed to get user information: timeout or network error");
} else { } else {
// Other errors // Other errors
throw new Error(`Failed to get user information: ${error.message}`); throw new ApiError(401, `Failed to get user information: ${error.message}`);
} }
} }
}; };

View File

@@ -8,7 +8,7 @@ export function errorHandler(err: any, req: Request, res: Response, next: NextFu
return res.status(err.status).json({code: err.status, message: err.message, data: null}); return res.status(err.status).json({code: err.status, message: err.message, data: null});
} }
// tsoa 内部生成的验证错误 // Validation errors generated by tsoa
if (err.status && err.status >= 400) { if (err.status && err.status >= 400) {
return res.status(err.status).json({ message: err.message }); return res.status(err.status).json({ message: err.message });
} }

View File

@@ -2,11 +2,18 @@ import swaggerUi from 'swagger-ui-express';
import path from 'path'; import path from 'path';
export function RegisterTsoaRoutes(app: any) { export function RegisterTsoaRoutes(app: any) {
// Register tsoa routes - 动态导入以避免编译时错误 // Register tsoa routes via dynamic import to avoid compile-time errors
const { RegisterRoutes } = require('../routes/routes'); const { RegisterRoutes } = require('../routes/routes');
RegisterRoutes(app); RegisterRoutes(app);
// Serve swagger documentation - 动态导入swagger文档 // Serve swagger documentation (dynamic import)
const swaggerDocument = require(path.join(__dirname, '../swagger.json')); const swaggerDocument = require(path.join(__dirname, '../swagger.json'));
app.use('/v3/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); const options = {
swaggerOptions: {
url: "/api/swagger/swagger.json",
},
}
app.get("/api/swagger/swagger.json", (req: any, res: any) => res.json(swaggerDocument));
// @ts-ignore
app.use('/api/swagger', swaggerUi.serveFiles(null, options), swaggerUi.setup(null, options));
} }

View File

@@ -1,6 +1,18 @@
/**
* Standard API response structure
*/
export interface ApiResponse<T = any> { export interface ApiResponse<T = any> {
/**
* HTTP status code, e.g., 200 for success, 400 for client error, 500 for server error
*/
code: number; code: number;
/**
* Response message, e.g., "success" or error description
*/
message: string; message: string;
/**
* Response data; can be null if no data is returned
*/
data: T | null; data: T | null;
} }

View File

@@ -1,11 +1,41 @@
/**
* User information interface
*/
export interface UserInfo { export interface UserInfo {
/**
* User ID
*/
userId: number; userId: number;
/**
* User nickname
*/
nickname: string; nickname: string;
/**
* Avatar URL
* @example "https://example.com/avatar.jpg"
*/
avatarUrl: string; avatarUrl: string;
gender: 'MALE' | 'FEMALE' | 'UNKNOWN'; gender: 'MALE' | 'FEMALE' | 'UNKNOWN';
nimToken: string; // NetEase Cloud Communication token /**
nimAccountId: string; // NetEase Cloud Communication account ID * NetEase Cloud Communication token
createdAt: string; */
nimToken: string;
/**
* NetEase Cloud Communication account ID
*/
nimAccountId: string;
/**
* Authentication token
*/
token: string;
/**
* Last update time yyyy-MM-dd HH:mm:ss
* @example "2023-10-05 14:48:00"
*/
updatedAt: string; updatedAt: string;
token: string; // Authentication token /**
* Account creation time yyyy-MM-dd HH:mm:ss
* @example "2023-10-05 14:48:00"
*/
createdAt: string;
} }