Compare commits
17 Commits
7c1241b26c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d474216047 | ||
|
|
71a7d7ba8c | ||
|
|
bfaa656406 | ||
|
|
b293b91697 | ||
|
|
5495c7bf25 | ||
|
|
2f38d95e8c | ||
|
|
defe5ac49b | ||
|
|
97bd5d013b | ||
|
|
1659d03040 | ||
|
|
99c8fa5f2d | ||
|
|
7f057a50ca | ||
|
|
b19b9f8b5f | ||
|
|
339dccdaa5 | ||
|
|
c6464fe1d1 | ||
|
|
2341e36aa9 | ||
|
|
2651bd1373 | ||
|
|
eb6eceffe9 |
28
AGENTS.md
28
AGENTS.md
@@ -14,6 +14,8 @@
|
|||||||
- 基于响应式设计的单页应用(SPA)
|
- 基于响应式设计的单页应用(SPA)
|
||||||
- 专为移动端优化,**采用单一入口点架构**,所有功能模块通过组件化方式在同一页面内动态加载和切换,确保用户体验的连贯性和加载性能的优化
|
- 专为移动端优化,**采用单一入口点架构**,所有功能模块通过组件化方式在同一页面内动态加载和切换,确保用户体验的连贯性和加载性能的优化
|
||||||
- **主题适配**: 确保组件支持亮色/暗色主题
|
- **主题适配**: 确保组件支持亮色/暗色主题
|
||||||
|
- 确保页面背景色、文字色、按钮色等UI元素在不同主题下均有良好对比度和可读性
|
||||||
|
- 但是页面上不要展示主题切换按钮,主题切换根据url参数 `?theme=dark` 或 `?theme=light` 来控制
|
||||||
- **错误展示**:前端页面需要有统一的错误展示,用于显示API请求失败或其他操作错误的信息
|
- **错误展示**:前端页面需要有统一的错误展示,用于显示API请求失败或其他操作错误的信息
|
||||||
- 不需要实现登录页,默认访问应用的用户都是已登录状态
|
- 不需要实现登录页,默认访问应用的用户都是已登录状态
|
||||||
- 所有请求统一使用 `/src/api/index` 中的 `api` 方法进行调用,因为已经内置了必要的请求头封装
|
- 所有请求统一使用 `/src/api/index` 中的 `api` 方法进行调用,因为已经内置了必要的请求头封装
|
||||||
@@ -24,6 +26,19 @@
|
|||||||
- 所有接口以 `/api` 为前缀
|
- 所有接口以 `/api` 为前缀
|
||||||
- 所有接口按照 `tsoa` 规范来编写,参考 `src/controllers` 目录下的 `UserController`、`GroupController` 示例
|
- 所有接口按照 `tsoa` 规范来编写,参考 `src/controllers` 目录下的 `UserController`、`GroupController` 示例
|
||||||
- 如果接口需要登录,请使用 `@Security('jwt')` 装饰器,tsoa 会自动调用登录校验,并把 user 信息写到req上,参考 `UserController` 示例
|
- 如果接口需要登录,请使用 `@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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,7 +51,7 @@ export const apiRequest = async <T = any>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 便捷的HTTP方法封装
|
// Convenience HTTP helpers
|
||||||
export const api = {
|
export const api = {
|
||||||
get: <T = any>(endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>> =>
|
get: <T = any>(endpoint: string, options?: RequestOptions): Promise<ApiResponse<T>> =>
|
||||||
apiRequest<ApiResponse<T>>(endpoint, { ...options, method: 'GET' }),
|
apiRequest<ApiResponse<T>>(endpoint, { ...options, method: 'GET' }),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export type UserInfo = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前用户的信息
|
* Get current user information
|
||||||
*/
|
*/
|
||||||
export const getUserInfo = async (): Promise<UserInfo | null> => {
|
export const getUserInfo = async (): Promise<UserInfo | null> => {
|
||||||
const res = await api.get<UserInfo>('/api/user/info');
|
const res = await api.get<UserInfo>('/api/user/info');
|
||||||
@@ -20,7 +20,7 @@ export const getUserInfo = async (): Promise<UserInfo | null> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取群组内所有用户的信息
|
* Get all users in the current group
|
||||||
*/
|
*/
|
||||||
export const getGroupMembers = async (): Promise<UserInfo[]> => {
|
export const getGroupMembers = async (): Promise<UserInfo[]> => {
|
||||||
const res = await api.get<UserInfo[]>('/api/group/members');
|
const res = await api.get<UserInfo[]>('/api/group/members');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,21 +19,21 @@ export const getGroupIdFromUrl = (): string | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存authToken到localStorage
|
* 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存groupId到localStorage
|
* 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 => {
|
||||||
const tokenFromUrl = getAuthTokenFromUrl();
|
const tokenFromUrl = getAuthTokenFromUrl();
|
||||||
@@ -41,7 +41,7 @@ export const getAuthToken = (): string | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从localStorage获取groupId
|
* Get groupId with URL fallback
|
||||||
*/
|
*/
|
||||||
export const getGroupId = (): string | null => {
|
export const getGroupId = (): string | null => {
|
||||||
const groupIdFromUrl = getGroupIdFromUrl();
|
const groupIdFromUrl = getGroupIdFromUrl();
|
||||||
@@ -49,22 +49,21 @@ export const getGroupId = (): string | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除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();
|
||||||
@@ -80,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');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"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": "yarn build && node ./build/index.js",
|
"serve": "yarn build && node ./build/index.js",
|
||||||
"start": "nodemon"
|
"start": "nodemon"
|
||||||
},
|
},
|
||||||
|
|||||||
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,4 +1,4 @@
|
|||||||
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";
|
||||||
@@ -40,11 +40,12 @@ export const getGroupMembers = async (groupId: number, token: string): Promise<U
|
|||||||
@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('/members')
|
@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')
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,11 +8,12 @@ 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')
|
||||||
|
|||||||
@@ -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 {RegisterTsoaRoutes} from "./middleware/tsoa.middleware"; // tsoa 生成的
|
import {RegisterTsoaRoutes} from "./middleware/tsoa.middleware";
|
||||||
|
|
||||||
const port = process.env.PORT || 3005;
|
const port = process.env.PORT || 3005;
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ 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
|
||||||
RegisterTsoaRoutes(app);
|
RegisterTsoaRoutes(app);
|
||||||
|
|||||||
@@ -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}`,
|
||||||
@@ -74,4 +74,3 @@ export const getUserInfoByToken = async (token: string): Promise<UserInfo> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user