Compare commits

...

10 Commits

Author SHA1 Message Date
dayjoy
74c09c1d81 feat: 添加错误展示说明 2025-09-12 16:57:49 +08:00
dayjoy
f8702e80f2 feat: 添加models、services目录 2025-09-12 16:06:05 +08:00
jackycheng
7b96b16bbf fix: 修改数据库初始化的代码防止ai编写时出现循环引用 2025-09-12 16:03:27 +08:00
dayjoy
b155df6567 feat: 禁止修改build命令 2025-09-12 15:34:21 +08:00
dayjoy
8c85fe943c 添加AGENTS.md 2025-09-12 14:52:51 +08:00
dayjoy
3be454ce38 feat(server): 使用sequelize.sync({ alter: true }),不管了 2025-09-12 11:01:39 +08:00
dayjoy
9a7549d18c feat(client): 获取url参数后更新url 2025-09-12 10:49:53 +08:00
dayjoy
0d4a938c00 feat: 增加群组用户列表获取功能 2025-09-11 16:46:54 +08:00
dayjoy
c782dea322 feat(server): 移除userservice 2025-09-11 15:38:20 +08:00
dayjoy
ddc4ac26bc feat(server): 添加用户信息获取逻辑 2025-09-11 14:02:25 +08:00
12 changed files with 1310 additions and 985 deletions

65
AGENTS.md Normal file
View File

@@ -0,0 +1,65 @@
## 核心目标
基于提供的工程模板架构智能解析用户的JSON格式需求自动生成完整的**node应用及移动端Web应用**。确保生成的代码具备以下特性:
- **完整性**: 包含前后端完整功能实现 (也可能没有前端,是纯后端项目)
- **可运行性**: 代码即拆即用,无需额外修改
- **规范性**: 严格遵循模板的技术栈和编码规范
- **部署就绪**: 自动构建生成可直接部署的工程产物
## 实现策略
### 1. 前端实现策略
- 基于响应式设计的单页应用SPA
- 专为移动端优化,**采用单一入口点架构**,所有功能模块通过组件化方式在同一页面内动态加载和切换,确保用户体验的连贯性和加载性能的优化
- **主题适配**: 确保组件支持亮色/暗色主题
- **错误展示**前端页面需要有统一的错误展示用于显示API请求失败或其他操作错误的信息
- 不需要实现登录页,默认访问应用的用户都是已登录状态
- 所有请求统一使用 `/src/api/index` 中的 `api` 方法进行调用,因为已经内置了必要的请求头封装
- 如果有需要,使用 `/src/api/user` 中的 `getUserInfo` 方法获取当前用户信息
- 如果有需要,使用 `/src/api/user` 中的 `getGroupUsers` 方法获取全部用户信息列表
### 2. 后端实现策略
- 所有接口以 `/api` 为前缀
- 已经为项目封装了当前用户信息获取的逻辑,提供了中间件,`userInfoMiddleware`,见: `src/middleware/auth.ts`
- 已经为项目封装了当前群组所有用户信息列表的获取逻辑,提供了中间件,`groupInfoMiddleware`,见: `src/middleware/auth.ts`
### 3. 开发规范
- **文件命名**: 使用kebab-case命名文件和文件夹
- **组件命名**: React组件使用PascalCase
- **API设计**: 遵循RESTful设计原则返回统一的JSON格式
- **类型定义**: 为所有数据结构定义TypeScript类型
- **代码组织**: 保持清晰的目录结构和模块化设计
## 任务执行流程
1. **需求分析**: 仔细解析JSON需求识别核心功能点
2. **架构设计**: 基于模板结构规划实现方案
3. **数据建模**: 根据data_models设计数据库表结构
4. **API设计**: 根据api_endpoints设计RESTful接口
5. **前端开发**: 实现页面组件、路由、状态管理
6. **后端开发**: 实现数据模型、API端点、业务逻辑
7. **build**: 调用`build`命令生成可部署的工程产物
* 如果报错则修正错误后再次build直到成功
* 禁止修改build命令
## 用户需求修改
请注意,用户可能会多次修改需求,**每次修改后用户会重新给你最新的完整的JSON需求描述**。
用户每次修改后,请在现有代码基础上**针对JSON中较上一次有变动的地方重点编辑实现**,确保所有变更都被正确反映。
## 注意事项
- 严格遵循现有的项目结构和编码规范
- 确保前端页面适配移动端访问(响应式)
- 保持API响应格式的一致性
- 及时更新相关的TypeScript类型定义
- 确保所有新增的页面都能正确路由和访问
---
请基于以上规范和用户JSON需求生成完整的、可运行的、符合输出格式要求的代码实现。
接下来我会输出用户JSON需求其中前端需求放在 `webapp_requirements`, 后端需求放在 `node_backend_requirements`
> 如果 webapp_requirements 中 is_needed = false代表不需要前端只需要生成后端代码

View File

@@ -0,0 +1,29 @@
import api from "@/api/index.ts";
import {getGroupId} from "@/utils/auth.ts";
export type UserInfo = {
userId: number;
nickname: string;
avatarUrl: string;
gender: 'MALE' | 'FEMALE' | 'UNKNOWN';
nimToken: string; // NetEase Cloud Communication token
nimAccountId: string; // NetEase Cloud Communication account ID
createdAt: string;
updatedAt: string;
}
/**
* 获取当前用户的信息
*/
export const getUserInfo = async (): Promise<UserInfo> => {
return await api.post<UserInfo>('https://egret.byteawake.com/api/user/info');
};
/**
* 获取群组内所有用户的信息
*/
export const getGroupUsers = async (): Promise<UserInfo[]> => {
return await api.post<UserInfo[]>('https://egret.byteawake.com/api/group/members', {
groupId: Number(getGroupId()),
});
};

View File

@@ -67,14 +67,22 @@ export const clearGroupId = (): void => {
export const handleAuthTokenAndGroupIdFromUrl = (): void => {
const tokenFromUrl = getAuthTokenFromUrl();
const groupIdFromUrl = getGroupIdFromUrl();
if (tokenFromUrl && groupIdFromUrl) {
saveAuthToken(tokenFromUrl);
saveGroupId(groupIdFromUrl);
// 从URL中移除authToken参数
let updated = false;
if (tokenFromUrl) {
saveAuthToken(tokenFromUrl);
updated = true;
}
if (groupIdFromUrl) {
saveGroupId(groupIdFromUrl);
updated = true;
}
// Clean URL if we consumed any param
if (updated) {
const url = new URL(window.location.href);
url.searchParams.delete('authToken');
url.searchParams.delete('groupId');
window.history.replaceState({}, '', url.toString());
window.history.replaceState(null, '', url.toString());
}
};

View File

@@ -12,6 +12,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.11.0",
"compression": "^1.7.5",
"cors": "^2.8.5",
"express": "^4.21.2",
@@ -21,6 +22,7 @@
"sqlite3": "^5.1.7"
},
"devDependencies": {
"@types/axios": "^0.9.36",
"@types/compression": "^1.7.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.13",

View File

@@ -1,4 +1,5 @@
import { Express } from "express";
import {userInfoMiddleware, groupInfoMiddleware} from "@/middleware/auth";
export const createApis = (app: Express) => {
app.get("/api/test", async (req, res) => {
@@ -8,5 +9,16 @@ export const createApis = (app: Express) => {
});
});
// 同时使用用户信息和群组信息中间件
app.get("/api/user-group/info", userInfoMiddleware, groupInfoMiddleware, async (req, res) => {
res.status(200).json({
message: "success",
data: {
user: req.userInfo,
group: req.groupInfo
}
});
});
/** add apis here */
};

View File

@@ -1,10 +1,4 @@
import path from "path";
import { Sequelize } from "sequelize";
export const sequelize = new Sequelize({
dialect: "sqlite",
storage: path.resolve(__dirname, process.env.DB_PATH || "./data.sqlite"),
});
import { sequelize } from "./instance";
async function initializeDatabase() {
try {

View File

@@ -0,0 +1,7 @@
import path from "path";
import { Sequelize } from "sequelize";
export const sequelize = new Sequelize({
dialect: "sqlite",
storage: path.resolve(__dirname, process.env.DB_PATH || "./data.sqlite"),
});

View File

@@ -12,6 +12,7 @@ import expressOasGenerator, {
dotenv.config({ path: path.resolve(__dirname, "../../../.env") });
import "./database";
import { sequelize } from "@/database/instance";
import { createApis } from "./api";
const port = process.env.PORT || 3005;
@@ -50,6 +51,12 @@ app.get("/v3/api-docs", async (req, res) => {
});
const host = "0.0.0.0";
app.listen(Number(port), host, () => {
app.listen(Number(port), host, async () => {
try {
await sequelize.sync({ alter: true });
console.log("[server]: sequelize.sync() executed");
} catch (e) {
console.error("Failed to sync database:", e);
}
console.log(`[server]: Server is running at http://${host}:${port}`);
});

View File

@@ -0,0 +1,178 @@
import {Request, Response, NextFunction} from "express";
import axios from "axios";
export type UserInfo = {
userId: number;
nickname: string;
avatarUrl: string;
gender: 'MALE' | 'FEMALE' | 'UNKNOWN';
nimToken: string; // NetEase Cloud Communication token
nimAccountId: string; // NetEase Cloud Communication account ID
createdAt: string;
updatedAt: string;
}
declare global {
namespace Express {
interface Request {
userInfo?: UserInfo | null;
groupInfo?: {
groupId: number;
users: UserInfo[];
} | null;
}
}
}
/**
* Extract Bearer token from request header
*/
export const extractTokenFromHeader = (authHeader: string | undefined): string | null => {
if (!authHeader) {
return null;
}
const parts = authHeader.split(" ");
if (parts.length !== 2 || parts[0] !== "Bearer") {
return null;
}
return parts[1];
};
/**
* Get user information by token
*/
export const getUserInfoByToken = async (token: string): Promise<any> => {
try {
const response = await axios.post(
"https://egret.byteawake.com/api/user/info",
{}, // 请求体数据,这里为空对象
{
headers: {
Authorization: `Bearer ${token}`,
},
timeout: 10000, // 10 second timeout
}
);
return response.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}`);
} else if (error.request) {
// Request was sent but no response received
throw new Error("Failed to get user information: timeout or network error");
} else {
// Other errors
throw new Error(`Failed to get user information: ${error.message}`);
}
}
};
/**
* User information middleware
* Automatically extract token from request header and get user information, store result in req.userInfo
*/
export const userInfoMiddleware = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const authHeader = req.headers.authorization;
const token = extractTokenFromHeader(authHeader);
if (!token) {
res.status(401).json({
error: "No valid access token provided",
message: "Missing Bearer token in Authorization header",
});
return;
}
// Get user information
const userInfoRes = await getUserInfoByToken(token);
req.userInfo = userInfoRes.code === 200 ? userInfoRes.data : null;
next();
} catch (error: any) {
res.status(401).json({
error: "User authentication failed",
message: error.message,
});
}
};
//////// group
/**
* Get group users
*/
export const getGroupUsers = async (groupId: number, token: string): Promise<any> => {
try {
const response = await axios.post<UserInfo[]>(
"https://egret.byteawake.com/api/group/members",
{groupId},
{
headers: {
Authorization: `Bearer ${token}`,
},
timeout: 10000, // 10 second timeout
}
);
return response.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}`);
} else if (error.request) {
// Request was sent but no response received
throw new Error("Failed to get user information: timeout or network error");
} else {
// Other errors
throw new Error(`Failed to get user information: ${error.message}`);
}
}
};
/**
* group information middleware
*/
export const groupInfoMiddleware = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const authHeader = req.headers.authorization;
const token = extractTokenFromHeader(authHeader);
const groupId = req.headers.groupid;
if (token && groupId) {
try {
const usersRes = await getGroupUsers(Number(groupId), token);
const users: UserInfo[] = usersRes.code === 200 ? usersRes.data : [];
req.groupInfo = {
groupId: Number(groupId),
users,
}
} catch (error) {
// If getting user information fails, do not block the request from continuing, but do not set userInfo
console.warn("Failed to get group user information:", error);
}
}
next();
} catch (error: any) {
res.status(400).json({
error: "Get Group Users failed",
message: error.message,
});
}
};

View File

View File

1967
yarn.lock

File diff suppressed because it is too large Load Diff