Compare commits
28 Commits
8c60050ff1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d474216047 | ||
|
|
71a7d7ba8c | ||
|
|
bfaa656406 | ||
|
|
b293b91697 | ||
|
|
5495c7bf25 | ||
|
|
2f38d95e8c | ||
|
|
defe5ac49b | ||
|
|
97bd5d013b | ||
|
|
1659d03040 | ||
|
|
99c8fa5f2d | ||
|
|
7f057a50ca | ||
|
|
b19b9f8b5f | ||
|
|
339dccdaa5 | ||
|
|
c6464fe1d1 | ||
|
|
2341e36aa9 | ||
|
|
2651bd1373 | ||
|
|
eb6eceffe9 | ||
|
|
7c1241b26c | ||
|
|
718a535cb1 | ||
|
|
eb39f41f0c | ||
|
|
a4ccdada1c | ||
|
|
d8a921b50a | ||
|
|
a5048bafb3 | ||
|
|
999a8e2520 | ||
|
|
b78a07f192 | ||
|
|
5688bb4242 | ||
|
|
b1530ce06e | ||
|
|
f53aed1b82 |
34
AGENTS.md
34
AGENTS.md
@@ -14,16 +14,31 @@
|
||||
- 基于响应式设计的单页应用(SPA)
|
||||
- 专为移动端优化,**采用单一入口点架构**,所有功能模块通过组件化方式在同一页面内动态加载和切换,确保用户体验的连贯性和加载性能的优化
|
||||
- **主题适配**: 确保组件支持亮色/暗色主题
|
||||
- 确保页面背景色、文字色、按钮色等UI元素在不同主题下均有良好对比度和可读性
|
||||
- 但是页面上不要展示主题切换按钮,主题切换根据url参数 `?theme=dark` 或 `?theme=light` 来控制
|
||||
- **错误展示**:前端页面需要有统一的错误展示,用于显示API请求失败或其他操作错误的信息
|
||||
- 不需要实现登录页,默认访问应用的用户都是已登录状态
|
||||
- 所有请求统一使用 `/src/api/index` 中的 `api` 方法进行调用,因为已经内置了必要的请求头封装
|
||||
- 如果有需要,使用 `/src/api/user` 中的 `getUserInfo` 方法获取当前用户信息
|
||||
- 如果有需要,使用 `/src/api/user` 中的 `getGroupUsers` 方法获取全部用户信息列表
|
||||
- 如果有需要,使用 `/src/api/user` 中的 `getGroupMembers` 方法获取全部用户信息列表
|
||||
|
||||
### 2. 后端实现策略
|
||||
- 所有接口以 `/api` 为前缀
|
||||
- 已经为项目封装了当前用户信息获取的逻辑,提供了中间件,`userInfoMiddleware`,见: `src/middleware/auth.ts`
|
||||
- 已经为项目封装了当前群组所有用户信息列表的获取逻辑,提供了中间件,`groupInfoMiddleware`,见: `src/middleware/auth.ts`
|
||||
- 所有接口按照 `tsoa` 规范来编写,参考 `src/controllers` 目录下的 `UserController`、`GroupController` 示例
|
||||
- 如果接口需要登录,请使用 `@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. 开发规范
|
||||
- **文件命名**: 使用kebab-case命名文件和文件夹
|
||||
@@ -32,6 +47,11 @@
|
||||
- **类型定义**: 为所有数据结构定义TypeScript类型
|
||||
- **代码组织**: 保持清晰的目录结构和模块化设计
|
||||
|
||||
### 4. 设计规范
|
||||
|
||||
- **响应式**: 确保所有页面在不同屏幕尺寸下均有良好展示效果
|
||||
- **滚动条**: 根据屏幕宽度变化自适应,🈲禁止出现横向滚动条
|
||||
|
||||
## 任务执行流程
|
||||
|
||||
1. **需求分析**: 仔细解析JSON需求,识别核心功能点
|
||||
@@ -50,6 +70,8 @@
|
||||
|
||||
用户每次修改后,请在现有代码基础上**针对JSON中较上一次有变动的地方重点编辑实现**,确保所有变更都被正确反映。
|
||||
|
||||
此外用户还可能通过 `appBug` 字段反馈bug,**如果 `appBug` 字段不为空字符串,则必须优先修复对应的bug**,并确保修复后代码依然符合用户最新的JSON需求。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 严格遵循现有的项目结构和编码规范
|
||||
@@ -62,7 +84,9 @@
|
||||
|
||||
请基于以上规范和用户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
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
],
|
||||
"scripts": {
|
||||
"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:server": "yarn workspace egret-app-server start",
|
||||
"start:server": "yarn workspace egret-app-server serve",
|
||||
"build": "lerna run build && node ./scripts/post-build.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -2,15 +2,30 @@ import "./App.css";
|
||||
import React, { useEffect } from "react";
|
||||
import { Routes } from "react-router-dom";
|
||||
import {handleAuthTokenAndGroupIdFromUrl} from "./utils/auth";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
|
||||
const App: React.FC = () => {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
// 在组件挂载时处理URL中的authToken 和 groupId
|
||||
// Consume authToken & groupId from URL and persist
|
||||
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 (
|
||||
<div className="w-full h-full bg-gray-50">
|
||||
<div className="w-full min-h-dvh bg-background text-foreground">
|
||||
<main>
|
||||
<Routes></Routes>
|
||||
</main>
|
||||
|
||||
@@ -5,8 +5,7 @@ interface RequestOptions extends RequestInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的fetch请求封装
|
||||
* 自动添加Authorization头部
|
||||
* Unified fetch wrapper that adds auth and group headers
|
||||
*/
|
||||
export const apiRequest = async <T = any>(
|
||||
endpoint: string,
|
||||
@@ -17,34 +16,34 @@ export const apiRequest = async <T = any>(
|
||||
|
||||
const url = endpoint;
|
||||
|
||||
// 准备请求头
|
||||
// Prepare request headers
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
// 如果有authToken,添加Authorization头
|
||||
// Attach Authorization header if token exists
|
||||
if (authToken) {
|
||||
headers.authorization = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
// 如果有groupId,添加groupId头
|
||||
// Attach groupId header if present
|
||||
if (groupId) {
|
||||
headers.groupId = groupId;
|
||||
}
|
||||
|
||||
// 发起请求
|
||||
// Fire request
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
// 处理响应
|
||||
// Handle non-2xx
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 尝试解析JSON,如果失败则返回响应对象
|
||||
// Try to parse JSON; fallback to raw response
|
||||
try {
|
||||
return await response.json();
|
||||
} catch {
|
||||
@@ -52,34 +51,40 @@ export const apiRequest = async <T = any>(
|
||||
}
|
||||
};
|
||||
|
||||
// 便捷的HTTP方法封装
|
||||
// Convenience HTTP helpers
|
||||
export const api = {
|
||||
get: <T = any>(endpoint: string, options?: RequestOptions): Promise<T> =>
|
||||
apiRequest<T>(endpoint, { ...options, method: 'GET' }),
|
||||
get: <T = any>(endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>> =>
|
||||
apiRequest<ApiResponse<T>>(endpoint, { ...options, method: 'GET' }),
|
||||
|
||||
post: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> =>
|
||||
apiRequest<T>(endpoint, {
|
||||
post: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<ApiResponse<T>> =>
|
||||
apiRequest<ApiResponse<T>>(endpoint, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
}),
|
||||
|
||||
put: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> =>
|
||||
apiRequest<T>(endpoint, {
|
||||
put: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<ApiResponse<T>> =>
|
||||
apiRequest<ApiResponse<T>>(endpoint, {
|
||||
...options,
|
||||
method: 'PUT',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
}),
|
||||
|
||||
patch: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<T> =>
|
||||
apiRequest<T>(endpoint, {
|
||||
patch: <T = any>(endpoint: string, data?: any, options?: RequestOptions): Promise<ApiResponse<T>> =>
|
||||
apiRequest<ApiResponse<T>>(endpoint, {
|
||||
...options,
|
||||
method: 'PATCH',
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
}),
|
||||
|
||||
delete: <T = any>(endpoint: string, options?: RequestOptions): Promise<T> =>
|
||||
apiRequest<T>(endpoint, { ...options, method: 'DELETE' }),
|
||||
delete: <T = any>(endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>> =>
|
||||
apiRequest<ApiResponse<T>>(endpoint, { ...options, method: 'DELETE' }),
|
||||
};
|
||||
|
||||
export type ApiResponse<T> = {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import api from "@/api/index.ts";
|
||||
import {getGroupId} from "@/utils/auth.ts";
|
||||
|
||||
export type UserInfo = {
|
||||
userId: number;
|
||||
@@ -13,17 +12,17 @@ export type UserInfo = {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的信息
|
||||
* Get current user information
|
||||
*/
|
||||
export const getUserInfo = async (): Promise<UserInfo> => {
|
||||
return await api.post<UserInfo>('https://egret.byteawake.com/api/user/info');
|
||||
export const getUserInfo = async (): Promise<UserInfo | null> => {
|
||||
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[]> => {
|
||||
return await api.post<UserInfo[]>('https://egret.byteawake.com/api/group/members', {
|
||||
groupId: Number(getGroupId()),
|
||||
});
|
||||
export const getGroupMembers = async (): Promise<UserInfo[]> => {
|
||||
const res = await api.get<UserInfo[]>('/api/group/members');
|
||||
return res.code === 200 ? res.data || [] : [];
|
||||
};
|
||||
|
||||
@@ -130,3 +130,18 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
// 处理认证相关的工具函数
|
||||
// Auth utilities
|
||||
|
||||
const AUTH_TOKEN_KEY = 'authToken';
|
||||
const GROUP_ID_KEY = 'groupId';
|
||||
|
||||
/**
|
||||
* 从URL搜索参数中获取authToken
|
||||
* Get authToken from URL search params
|
||||
*/
|
||||
export const getAuthTokenFromUrl = (): string | null => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('authToken');
|
||||
};
|
||||
/**
|
||||
* 从URL搜索参数中获取groupId
|
||||
* Get groupId from URL search params
|
||||
*/
|
||||
export const getGroupIdFromUrl = (): string | null => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -19,50 +19,51 @@ export const getGroupIdFromUrl = (): string | null => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存authToken到localStorage
|
||||
* Save authToken to localStorage
|
||||
*/
|
||||
export const saveAuthToken = (token: string): void => {
|
||||
localStorage.setItem(AUTH_TOKEN_KEY, token);
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存groupId到localStorage
|
||||
* Save groupId to localStorage
|
||||
*/
|
||||
export const saveGroupId = (groupId: string): void => {
|
||||
localStorage.setItem(GROUP_ID_KEY, groupId);
|
||||
};
|
||||
|
||||
/**
|
||||
* 从localStorage获取authToken
|
||||
* Get authToken with URL fallback
|
||||
*/
|
||||
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 => {
|
||||
return localStorage.getItem(GROUP_ID_KEY);
|
||||
const groupIdFromUrl = getGroupIdFromUrl();
|
||||
return groupIdFromUrl || localStorage.getItem(GROUP_ID_KEY);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除authToken
|
||||
* Clear authToken
|
||||
*/
|
||||
export const clearAuthToken = (): void => {
|
||||
localStorage.removeItem(AUTH_TOKEN_KEY);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除groupId
|
||||
* Clear groupId
|
||||
*/
|
||||
export const clearGroupId = (): void => {
|
||||
localStorage.removeItem(GROUP_ID_KEY);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查并处理URL中的authToken
|
||||
* 如果URL中有authToken参数,则保存到localStorage并从URL中移除
|
||||
* Consume authToken/groupId from URL and persist, then clean URL
|
||||
*/
|
||||
export const handleAuthTokenAndGroupIdFromUrl = (): void => {
|
||||
const tokenFromUrl = getAuthTokenFromUrl();
|
||||
@@ -78,7 +79,7 @@ export const handleAuthTokenAndGroupIdFromUrl = (): void => {
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// Clean URL if we consumed any param
|
||||
// Clean URL if any param was consumed
|
||||
if (updated) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('authToken');
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "tsoa -c tsoaConfig.json spec-and-routes && tsc && tsc-alias",
|
||||
"serve": "node ./build/index.js",
|
||||
"build": "tsoa -c tsoaConfig.json spec-and-routes && node scripts/clean-swagger.js && tsc && tsc-alias",
|
||||
"serve": "yarn build && node ./build/index.js",
|
||||
"start": "nodemon"
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
71
packages/server/scripts/clean-swagger.js
Normal file
71
packages/server/scripts/clean-swagger.js
Normal 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!");
|
||||
@@ -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 type {Request as ExpressRequest} from 'express';
|
||||
import axios from "axios";
|
||||
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 {
|
||||
const response = await axios.post<ApiResponse<UserInfo[]>>(
|
||||
"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) {
|
||||
throw new Error(`Failed to get group users: ${response.data.message}`);
|
||||
throw new Error(response.data.message);
|
||||
}
|
||||
|
||||
return response.data.data ?? [];
|
||||
} catch (error: any) {
|
||||
if (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) {
|
||||
// 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 {
|
||||
// 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')
|
||||
export class GroupController extends Controller {
|
||||
/**
|
||||
* 获取当前聊天群组的全部用户信息
|
||||
* @summary 获取当前聊天群组的全部用户信息
|
||||
* @description 获取当前聊天群组的全部用户信息
|
||||
* Get all users in the current chat group
|
||||
* @summary Get group members
|
||||
* @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")
|
||||
@Response<ApiResponse<UserInfo[]>>(200, 'Success')
|
||||
@Response<ApiResponse<null>>(400, 'Bad Request')
|
||||
@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 groupId = req.headers.groupid;
|
||||
|
||||
@@ -57,7 +58,7 @@ export class GroupController extends Controller {
|
||||
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 {
|
||||
code: 200,
|
||||
@@ -65,4 +66,4 @@ export class GroupController extends Controller {
|
||||
data: users,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@Route('api')
|
||||
@Tags('Test')
|
||||
export class TestController extends Controller {
|
||||
/**
|
||||
* 测试接口
|
||||
* @summary 测试接口
|
||||
* @description 用于测试API连通性的基础接口
|
||||
* Test endpoint
|
||||
* @summary Test endpoint
|
||||
* @description Basic endpoint to verify API connectivity
|
||||
*/
|
||||
@Get('/test')
|
||||
@OperationId('Test_GetTest') // MUST: Specify operationId for better API documentation
|
||||
@Response<ApiResponse>(200, 'Success')
|
||||
public async getTest(): Promise<ApiResponse> {
|
||||
return {
|
||||
|
||||
@@ -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 { Request as ExpressRequest } from 'express';
|
||||
import type {UserInfo} from "../types/user";
|
||||
@@ -8,19 +8,21 @@ import type {UserInfo} from "../types/user";
|
||||
@Tags('User')
|
||||
export class UserController extends Controller {
|
||||
/**
|
||||
* 获取当前用户的信息
|
||||
* @summary 获取当前用户的信息
|
||||
* @description 获取当前用户的信息
|
||||
* Get current user information
|
||||
* @summary Get current user information
|
||||
* @description Returns the authenticated user's profile
|
||||
*/
|
||||
@Get('/info')
|
||||
@OperationId('User_GetUserGroupInfo') // MUST: Specify operationId for better API documentation
|
||||
@Security('jwt')
|
||||
@Response<ApiResponse<UserInfo>>(200, 'Success')
|
||||
@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 {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: req.user
|
||||
data: rest
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ dotenv.config({ path: path.resolve(__dirname, "../../../.env") });
|
||||
import "./database";
|
||||
import { sequelize } from "./database/instance";
|
||||
import {errorHandler} from "./middleware/errorHandler";
|
||||
import {RegisterTsoaRoutes} from "./middleware/tsoa.middleware"; // tsoa 生成的
|
||||
import {RegisterTsoaRoutes} from "./middleware/tsoa.middleware";
|
||||
|
||||
const port = process.env.PORT || 3005;
|
||||
|
||||
@@ -20,7 +20,7 @@ app.use(nocache());
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: "100mb" }));
|
||||
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
|
||||
RegisterTsoaRoutes(app);
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function expressAuthentication(
|
||||
throw new ApiError(401, "Unauthorized");
|
||||
}
|
||||
|
||||
// 返回的对象会挂到 request.user 上
|
||||
// Returned object is assigned to request.user
|
||||
return await getUserInfoByToken(token);
|
||||
}
|
||||
throw new ApiError(401, "Unsupported security scheme");
|
||||
@@ -45,7 +45,7 @@ export const getUserInfoByToken = async (token: string): Promise<UserInfo> => {
|
||||
try {
|
||||
const response = await axios.post<ApiResponse<Omit<UserInfo, 'token'>>>(
|
||||
"https://egret.byteawake.com/api/user/info",
|
||||
{}, // 请求体数据,这里为空对象
|
||||
{}, // Empty request body
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -55,23 +55,22 @@ export const getUserInfoByToken = async (token: string): Promise<UserInfo> => {
|
||||
);
|
||||
|
||||
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};
|
||||
} catch (error: any) {
|
||||
if (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) {
|
||||
// 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 {
|
||||
// Other errors
|
||||
throw new Error(`Failed to get user information: ${error.message}`);
|
||||
throw new ApiError(401, `Failed to get user information: ${error.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
// tsoa 内部生成的验证错误
|
||||
// Validation errors generated by tsoa
|
||||
if (err.status && err.status >= 400) {
|
||||
return res.status(err.status).json({ message: err.message });
|
||||
}
|
||||
|
||||
@@ -2,11 +2,18 @@ import swaggerUi from 'swagger-ui-express';
|
||||
import path from 'path';
|
||||
|
||||
export function RegisterTsoaRoutes(app: any) {
|
||||
// Register tsoa routes - 动态导入以避免编译时错误
|
||||
// Register tsoa routes via dynamic import to avoid compile-time errors
|
||||
const { RegisterRoutes } = require('../routes/routes');
|
||||
RegisterRoutes(app);
|
||||
|
||||
// Serve swagger documentation - 动态导入swagger文档
|
||||
// Serve swagger documentation (dynamic import)
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
/**
|
||||
* Standard API response structure
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
/**
|
||||
* HTTP status code, e.g., 200 for success, 400 for client error, 500 for server error
|
||||
*/
|
||||
code: number;
|
||||
/**
|
||||
* Response message, e.g., "success" or error description
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Response data; can be null if no data is returned
|
||||
*/
|
||||
data: T | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,41 @@
|
||||
/**
|
||||
* User information interface
|
||||
*/
|
||||
export interface UserInfo {
|
||||
/**
|
||||
* User ID
|
||||
*/
|
||||
userId: number;
|
||||
/**
|
||||
* User nickname
|
||||
*/
|
||||
nickname: string;
|
||||
/**
|
||||
* Avatar URL
|
||||
* @example "https://example.com/avatar.jpg"
|
||||
*/
|
||||
avatarUrl: string;
|
||||
gender: 'MALE' | 'FEMALE' | 'UNKNOWN';
|
||||
nimToken: string; // NetEase Cloud Communication token
|
||||
nimAccountId: string; // NetEase Cloud Communication account ID
|
||||
createdAt: string;
|
||||
/**
|
||||
* NetEase Cloud Communication token
|
||||
*/
|
||||
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;
|
||||
token: string; // Authentication token
|
||||
/**
|
||||
* Account creation time yyyy-MM-dd HH:mm:ss
|
||||
* @example "2023-10-05 14:48:00"
|
||||
*/
|
||||
createdAt: string;
|
||||
}
|
||||
Reference in New Issue
Block a user