Compare commits

..

33 Commits

Author SHA1 Message Date
hnu202326010328 0357d9339d docs:提交用户手册
1 day ago
hnu202326010328 ef2964a046 上传文件至 'doc/project/05-测试报告'
1 day ago
hnu202326010328 c64bdd7d62 删除 'doc/project/05-测试报告/测试报告.pdf'
1 day ago
hnu202326010328 d95af47ebc docs:提交测试报告
1 day ago
smallbailangui e365930b32 docs: 更新UML顺序图
2 days ago
smallbailangui 82c73043f9 docs: 更新UML顺序图
2 days ago
smallbailangui 77626a979b test(backend): 重命名测试文件并删除 ddl 生成脚本
3 days ago
Teptao f7bcdafb07 Merge branch 'develop' of https://bdgit.educoder.net/hnu202326010328/202326010 into develop
5 days ago
Teptao 1efac00aa2 下拉框
5 days ago
smallbailangui df5ae7543f Merge remote-tracking branch 'origin/develop' into develop
5 days ago
smallbailangui 34da277dd8 fix: 更改已登出为登出
5 days ago
Teptao 58760f24f7 修改布局
5 days ago
Teptao 07e31768b9 修改布局
5 days ago
Teptao 501a77b5e5 用户详情界面优化
5 days ago
smallbailangui ada01300de Merge remote-tracking branch 'origin/develop' into develop
5 days ago
smallbailangui dc28610821 fix(admin): 优化API密钥和URL格式验证逻辑
5 days ago
Teptao 6d5a302b85 修复近期登录历史栏
5 days ago
smallbailangui cd29ced4ad feat(dashboard): 添加项目创建表单字段长度验证和显示
6 days ago
smallbailangui f1120aa371 fix(admin): 添加 AI 模型连接测试的 400 错误处理
6 days ago
smallbailangui cb488d4c06 fix(dashboard): 优化项目创建失败错误处理
6 days ago
smallbailangui c370f5d9aa fix(auth): 修复密码验证和用户信息显示问题
6 days ago
smallbailangui cb81ad91f1 refactor(user): 更新邮箱验证实现
6 days ago
ymr fa6747c3c3 docs: 提交第十五周个人周总结
6 days ago
lsbp 3873821a0d Merge remote-tracking branch 'origin/develop' into develop
6 days ago
lsbp 344b91a472 docs:添加梁峻耀第十五周周总结文档
6 days ago
hnu202326010328 778fc95c41 docs:提交第十五周小组会议纪要
6 days ago
hnu202326010328 c212f045db docs: 提交第十五周周总结文档
6 days ago
hnu202326010328 93f2722724 docs:提交王利蓉第十五周周总结文档
6 days ago
smallbailangui 7bdf0a72d4 docs: 添加李果霖第十五周周总结文档
6 days ago
Teptao 57ad5a88cf docs:提交李文韬15周周总结
6 days ago
smallbailangui 05f2c52caa chore(docker): 配置容器时区为上海时区
1 week ago
smallbailangui 3d43928fe7 fix(admin): 修复今日活跃用户文本显示
1 week ago
smallbailangui fce25c3601 feat(AdminStatus): 更新今日查询量标签为今日业务强度
1 week ago

@ -0,0 +1,120 @@
# 小组会议纪要-第15周
## 会议记录概要
**团队名称:** 3班-葫芦娃救bug
**指导老师:** 李友焕
**主 持 人:** 项目经理
**记录人员:** 王利蓉
**会议主题:** 项目收尾阶段工作部署与总结
**会议地点:** 线上会议
**会议时间:** 2026-01-03 19:00-20:00
**记录时间:** 2026-01-03 21:00
**参与人员:** 全体成员
---
## 会议内容
### 1. 项目收尾阶段工作进展同步
- **系统集成测试完成情况**
- 全系统集成测试已按计划完成,各模块功能验证通过
- 核心业务逻辑严密,模块间联动无冲突
- 测试中发现的主要问题已记录并修复
- **云端部署状态确认**
- 系统已成功部署至云服务器,运行环境稳定
- 云端环境下各项功能测试通过,性能符合预期
- 部署文档和运维说明已整理完成
- **文档编制工作进展**
- 用户手册编写完成,涵盖所有功能模块的操作指南
- 测试报告编写完成,包含测试用例和结果汇总
- 技术文档更新同步,确保与最终实现一致
### 2. 下一阶段工作安排
- **最终评审准备**
- 整理所有交付物,包括代码、文档、演示材料等
- 准备项目总结汇报材料
- 安排最终演示彩排
- **代码仓库整理**
- 清理临时分支,合并主分支
- 完善代码注释和README文档
- 整理依赖包和配置文件
- **项目经验总结**
- 各成员整理个人工作经验和收获
- 团队总结项目开发过程中的经验教训
### 3. 质量保证措施确认
- **代码质量复核**
- 全员进行代码规范性最终检查
- 确保符合Google编码规范要求
- 清理所有硬编码和不规范写法
- **系统稳定性验证**
- 进行压力测试和性能测试
- 验证云端环境下的系统稳定性
- 准备应急预案和问题排查
- **文档完整性检查**
- 确认所有文档格式规范、内容完整
- 检查文档与实际实现的一致性
- 确保文档可读性和实用性
### 4. 团队协作与交流
- **沟通协调机制**
- 保持团队成员间及时沟通
- 建立问题快速响应机制
- 确保信息同步和资源共享
- **责任分工确认**
- 各成员明确收尾阶段职责
- 确保工作按时保质完成
---
## 问题总结
### 已解决问题:
1. 系统集成测试顺利完成,发现的问题已修复
2. 云端部署成功,系统运行稳定
3. 用户手册和测试报告编制完成
4. 代码规范性得到进一步提升
### 待解决问题:
1. 最终评审材料的完善和优化
2. 项目经验总结的深度挖掘
3. 代码仓库的最终整理和归档
---
## 小组协作情况总结
**协作情况:** 本周团队在项目收尾阶段协作高效,各成员认真负责,积极配合,确保了各项收尾工作的顺利进行。在测试、部署和文档工作中展现了良好的团队协作精神。
## 一周纪律情况总结
**纪律情况:** 全体成员按时参加会议,认真参与讨论,工作态度端正。在时间紧张的情况下,能够合理安排工作,保证项目进度。
---
## 备注
1. 本周是项目开发的最后冲刺阶段,需要全体成员保持高度专注和责任心
2. 各项收尾工作需按计划推进,确保项目顺利交付
3. 建议团队成员及时总结经验,为今后项目开发积累宝贵经验
---
## 【注】
1. 本文档为"葫芦娃救bug"小组第15周会议记录记录了项目收尾阶段的工作部署情况
2. 会议决议和任务安排已明确,各成员需按时完成分配的工作
3. 下周将进行项目最终评审,请全体成员做好准备
4. 文档已按要求整理并提交,相关材料已同步至代码托管平台

@ -0,0 +1,35 @@
# 小组周总结-第15周
## 团队名称和起止时间
**团队名称:** 3班-葫芦娃救bug
**开始时间:** 2025-12-29
**结束时间:** 2026-01-04
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
|------|----------|----------|----------|
| 1 | 完善风控模块 | 完成 | 伊木然完成系统自动风险用户判断功能,管理员可基于系统判断进行手动封禁操作 |
| 2 | 全系统集成测试 | 完成 | 全体成员参与系统集成测试,覆盖所有已完成模块,核心业务链路验证通过 |
| 3 | 修复前端BUG | 完成 | 前端人员解决界面布局适配问题及测试发现的UI交互缺陷提升系统可用性 |
| 4 | 修复后端BUG | 完成 | 后端人员修复测试过程中发现的业务逻辑漏洞,系统稳定性显著提升 |
| 5 | 部署云服务器 | 完成 | 李文韬完成云端环境配置和系统部署,系统在云端运行稳定 |
| 6 | 用户手册编写 | 完成 | 李果霖、伊木然完成用户手册编写,涵盖功能说明和常见问题处理,为操作提供清晰指引 |
| 7 | 测试报告编写 | 完成 | 梁峻耀、李文韬、王利蓉完成测试用例编写和集成测试结果汇总 |
## 小结
1. **系统稳定性达标**通过全面的集成测试和BUG修复系统各功能模块在联动时业务逻辑严密、运行稳定
2. **云端部署成功**:系统成功从本地开发环境迁移至云服务器,在真实云运行环境下表现稳定可靠;
3. **文档成果丰富**:用户手册和测试报告同步完成,为软件交付提供完整的技术和用户文档支持;
4. **遗留问题解决**:前端历史遗留的组件变形、布局错乱等问题得到有效解决,系统视觉稳定性大幅提升;
5. **团队协作高效**:全体成员在测试、修复和文档编写过程中密切配合,展现了良好的团队协作精神。
---
## 【注】
1. 集成测试过程中发现的BUG已及时记录并修复系统整体质量符合预期
2. 用户手册编写充分考虑了非技术背景用户的需求,操作指南清晰易懂;
3. 云服务器部署为系统正式上线运行提供了可靠的环境保障;

@ -0,0 +1,22 @@
# 个人周总结 - 第15周
**姓  名**:梁峻耀
**团队名称**3班-葫芦娃救bug
**开始时间**2025-12-29
**结束时间**2026-01-04
## 本周任务完成情况
| 序号 | 计划内容 | 是否完成 | 情况说明 |
| ---- | -------------- | -------- | ------------------------------------------------------------ |
| 1 | 测试报告编写 | 完成 | 与李文韬、王利蓉协作编写了覆盖12+核心接口的测试用例设计了结构化的测试报告模板包含本地vs云端性能对比数据针对风控模块、AI队列等核心功能进行了全面验证测试问题闭环率达95%。 |
| 2 | 后端BUG修复 | 进行中 | 重点解决了数据库适配后的兼容性问题修复了3处双数据库环境下的逻辑漏洞优化了全局异常处理模块在极端情况下的响应机制重构了2处缓存失效逻辑确保Redis与Celery任务状态一致性。 |
| 3 | 全系统集成测试 | 完成 | 主导设计了多场景集成测试用例覆盖用户注册到AI分析完整业务链路验证了Sqlite/PostgreSQL双数据库环境下的系统稳定性针对测试发现的5项关键问题提出了解决方案并完成修复。 |
## 小结
**核心进展**
- 高质量完成测试报告,包含详尽的性能对比数据和问题追踪,为系统上线提供质量保障;
- 系统性地解决了多数据库适配后的遗留问题,提升了系统在不同环境下的兼容性和稳定性;
- 建立了完善的测试验证体系,为后续版本迭代奠定了质量基础。

@ -0,0 +1,27 @@
# 个人周总结-第15周
## 姓名与起止时间
姓名:李果霖
团队名称3班-葫芦娃救bug
开始时间2025-12-29
结束时间2026-01-04
## 本周任务完成情况
| **序号** | **计划内容** | **完成情况** | **情况说明** |
| -------- | ------------------------ | ------------ | ------------ |
| 1 | **用户手册编写** | **已完成** | 与伊木然联合完成核心业务功能的用户手册章节补充RAG检索、AI对话、ER图导出等操作指引按交付要求整理上线。 |
| 2 | **云端部署技术支持** | **已完成** | 配合李文韬完成云服务器Celery异步任务队列与PostgreSQL/SQLite多数据库配置校验确认本地高级特性在云端稳定运行。 |
| 3 | **全系统集成测试** | **已完成** | 参与全链路测试重点验证RAG检索准确性、ER图生成与异步调度在云端表现测试结果与预期一致。 |
| 4 | **后端BUG修复** | **已完成** | 针对集成测试发现的异步任务与多库适配问题进行修复与回归验证,确保业务链路稳定。 |
| 5 | **代码规范最终审查** | **已完成** | 对负责模块执行收尾自查,清理临时调试代码与冗余注释,核对配置与文档一致性,符合代码规范要求。 |
## 小结
1. **交付收口**:完成核心功能的用户手册输出,并对文档与实现进行一致性核对,为最终评审提供可落地的用户指引。
2. **云端稳定性验证**:协助云端部署与多数据库、异步任务运行验证,确保核心特性在云环境无功能缺口。
3. **质量闭环**:通过集成测试与针对性修复消除异步和适配类问题,结合规范自查,整体模块在性能与质量上达到收尾标准。

@ -0,0 +1,31 @@
# 个人周总结-第15周
## 姓名与起止时间
**姓 名**:李文韬
**团队名称**3班-葫芦娃救bug
**开始时间**2025-12-29
**结束时间**2026-01-04
## 本周任务完成情况
| **序号** | **任务内容** | **是否完成** | **情况说明** |
| -------- | -------------- | ------------ | ------------------------------------------------------------ |
| 1 | 部署云服务器 | 已完成 | 完成云端环境配置和系统部署,确保系统在云端运行稳定。 |
| 2 | 测试报告编写 | 已完成 | 参与完成测试用例编写,并对集成测试结果进行了汇总。 |
| 3 | 全系统集成测试 | 已完成 | 全程参与系统集成测试,覆盖所有已完成模块,验证核心业务链路。 |
| 4 | 前后端BUG修复 | 已完成 | 配合团队解决界面布局适配问题及业务逻辑漏洞,提升系统稳定性。 |
| 5 | 一周总结 | 已完成 | 整理本周部署与测试成果,汇总集成测试报告,并完成了本份周总结。 |
## 小结
1. **部署任务达成:** 顺利完成从本地开发环境到云服务器的迁移,系统在真实云运行环境下表现稳定可靠,为正式上线提供了保障。
2. **质量全面把控:** 通过深度参与全系统集成测试与文档编写,及时修复了遗留的组件变形与布局错乱问题,系统整体质量符合预期。
## 【注】
1. **工作成果:** 云服务器配置已全部完成,目前系统运行环境稳定。
2. **团队协作:** 本周与团队成员在测试、修复和文档编写过程中密切配合,展现了高效的协作精神。

@ -0,0 +1,43 @@
# 个人周总结-第15周
## 姓名和起止时间
**姓  名:** 王利蓉
**团队名称:** 3班-葫芦娃救bug
**开始时间:** 2025-12-29
**结束时间:** 2026-01-04
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
|------|----------|----------|----------|
| 1 | 全系统集成测试参与 | 完成 | 积极参与系统集成测试重点验证了RAG检索、会话管理、报表和公告功能在集成环境下的稳定性和正确性所有核心流程测试通过 |
| 2 | 测试报告编写(核心模块) | 完成 | 完成了RAG检索、会话管理、报表、公告等核心模块的测试用例编写详细记录了测试步骤、预期结果和实际结果并汇总了集成测试结果 |
| 3 | 后端BUG修复支持 | 完成 | 协助修复了测试中发现的与负责模块相关的业务逻辑问题特别是优化了RAG检索的性能和准确性 |
| 4 | 风控模块测试支持 | 完成 | 配合伊木然完成了风控模块与现有功能的集成测试,确保风险用户判断逻辑不影响正常的用户业务流程 |
| 5 | 用户手册技术内容审核 | 完成 | 认真审核了用户手册中关于RAG检索、报表功能等复杂功能的技术描述确保表述准确、清晰易于用户理解 |
| 6 | 云端部署验证测试 | 完成 | 在李文韬完成云服务器部署后,及时对负责的功能模块进行了云端环境验证测试,确认功能运行稳定,数据交互正常 |
| 7 | 代码规范性最终检查 | 完成 | 对负责模块的代码进行了全面规范性检查,清理了遗留的硬编码和格式问题,确保代码质量符合项目评审要求 |
## 对团队工作的建议
1. **经验总结制度化:** 建议团队建立项目经验总结机制,将本次开发过程中的技术难点、解决方案和最佳实践进行系统整理,形成团队知识库;
2. **文档持续维护:** 建议建立文档定期更新和维护机制,确保技术文档、用户手册等随着系统迭代保持同步更新;
3. **代码审查常态化:** 建议在今后的项目中推行定期的代码交叉审查,持续提升团队的代码质量和协作效率;
## 小结
1. **测试工作完成:** 成功完成核心模块的集成测试和测试报告编写工作,为项目质量提供了有力保障;
2. **问题修复及时有效:** 积极配合团队修复测试中发现的问题特别是对RAG检索模块进行了性能优化
3. **文档质量严格把关:** 认真审核技术文档,确保用户手册内容准确、易懂;
4. **云端验证顺利通过:** 负责的功能模块在云端环境中运行稳定,满足部署要求;
5. **代码质量显著提升:** 通过最终代码规范性检查,个人负责模块的代码质量达到评审标准;
6. **团队协作高效顺畅:** 与团队成员在测试、修复和文档工作中配合默契,展现了良好的团队协作精神;
7. **项目收获丰富:** 通过本次项目,不仅提升了技术能力,还积累了宝贵的项目管理和团队协作经验。
---
## 【注】
1. **技术能力提升:** 希望未来能有机会参与更多关于系统性能优化和云端架构设计的专题学习;

@ -0,0 +1,27 @@
# 个人周总结-第15周
## 姓名与起止时间
**姓  名:** 伊木然
**团队名称:** 3班-葫芦娃救bug
**开始时间:** 2025-12-29
**结束时间:** 2026-01-04
## 本周任务完成情况
| **序号** | **计划内容** | **完成情况** | **情况说明** |
| -------- | -------------------- | ------------ | ------------------------------------------------------------ |
| 1 | **完善风控模块逻辑** | **完成** | **【核心产出】** 成功实现了“风险用户自动判定”的后端逻辑(如连续登录失败监测),并将异常状态与管理员的封禁功能打通,实现了“系统预警+人工审核”的安全闭环。 |
| 2 | **用户手册编写** | **完成** | 与李果霖协作完成了《用户手册》的编写重点负责了“账号管理”、“个人设置”及“管理员后台”章节的操作指南与FAQ整理为用户提供了清晰的文档支持。 |
| 3 | **后端Bug修复** | **完成** | 针对测试过程中发现的业务逻辑漏洞(特别是风控误判场景)进行了紧急修复,显著提升了系统的稳定性和数据的准确性。 |
| 4 | **配合云端部署** | **完成** | 协助李文韬解决了后端代码在Linux云服务器上的依赖兼容性问题如数据库驱动适配确保了后端服务在云端的顺利启动。 |
| 5 | **周总结与项目复盘** | **完成** | 整理了本周的风控代码和文档产出,撰写了个人周总结,并配合项目经理完成了项目最终交付物的汇总。 |
## 小结
1. **安全机制闭环**本周完成了Beta版本的最后一块拼图——风控模块使系统具备了自动识别和处理异常用户的能力安全性得到了质的提升。
2. **软硬交付同步**:在保障代码质量的同时,高质量完成了《用户手册》的编写,确保交付给用户的不仅是可运行的软件,还有易用的产品体验。
3. **使命必达**作为后端核心开发从环境搭建到Alpha验收再到Beta版的架构升级与最终交付始终保持了高效的输出圆满完成了本学期的开发任务。

@ -102,6 +102,8 @@ services:
volumes:
- ./src/backend:/app
environment:
# 时区设置
- TZ=Asia/Shanghai
# Fix import errors by adding inner app directory to PYTHONPATH
- PYTHONPATH=/app/app
# --- Pydantic BaseSettings 覆盖配置 (使用双下划线映射嵌套配置) ---

@ -575,6 +575,13 @@ async def test_ai_model_connection(
message="请求频率超限,请稍后再试",
response_time_ms=response_time_ms
)
elif response.status_code == 400:
# 400 错误通常是请求参数问题模型ID无效、密钥格式错误等
result = AIModelTestConnectionResponse(
success=False,
message="连接失败,请检查模型 ID 和 API 密钥是否正确",
response_time_ms=response_time_ms
)
else:
try:
resp_json = response.json()

@ -112,12 +112,14 @@ class PasswordInvalidException(BusinessException):
"""
user_id: int
failed_attempts: int
def __init__(self, user_id: int = 0, failed_attempts: int = 0, custom_message: str = None):
def __init__(self, user_id: int = 0, failed_attempts: int = 0, custom_message: str = None, is_login: bool = True):
if custom_message:
message = custom_message
else:
message = "密码错误"
super().__init__(code=401, message=message)
# 登录场景用401触发前端登出修改密码场景用400仅显示错误
status_code = 401 if is_login else 400
super().__init__(code=status_code, message=message)
self.user_id = user_id
self.failed_attempts = failed_attempts

@ -8,6 +8,7 @@ from pydantic import BaseModel, EmailStr, Field, field_validator, model_validato
from typing import Optional, Literal, List, Any
from datetime import datetime
import re
from email_validator import validate_email, EmailNotValidError
# 基础用户信息
@ -29,10 +30,10 @@ class UserBase(BaseModel):
return v
@validator('email')
def validate_email(cls, v):
def validate_email_format(cls, v):
try:
EmailStr.validate(v)
except Exception:
validate_email(v)
except EmailNotValidError:
raise ValueError('邮箱格式不正确,请输入有效的邮箱地址')
return v
@ -97,8 +98,8 @@ class UserUpdateEmailRequest(BaseModel):
@validator('new_email')
def validate_new_email(cls, v):
try:
EmailStr.validate(v)
except Exception:
validate_email(v)
except EmailNotValidError:
raise ValueError('邮箱格式不正确,请输入有效的邮箱地址')
return v
@ -121,10 +122,10 @@ class UserUpdateEmailConfirm(BaseModel):
return v
@validator('new_email')
def validate_new_email(cls, v):
def validate_new_email_format(cls, v):
try:
EmailStr.validate(v)
except Exception:
validate_email(v)
except EmailNotValidError:
raise ValueError('邮箱格式不正确,请输入有效的邮箱地址')
return v

@ -1,19 +0,0 @@
"""
DDL 生成脚本
用于根据需求与逻辑 Schema 调用远端服务生成 DDL适用于工具链与集成测试
"""
# backend/app/service/ddl_generator.py
import requests
url = "https://schema2ddl.strangeloop.fun/generate/ddl"
data = {
"database_requirment": "A university needs a student course selection management system to maintain and track students' course selection information. Students have\ninformation such as student ID, name, age, the name of the course chosen by the student, etc. Each student can take multiple courses and can drop or\nchange courses within the specified time. Each course has information such as course number, course name, credits, lecturer and class time. The\npopularity of a course depends on the number of students who take the course. The system can predict the popularity of the course and provide support\nfor academic decision-making",
"schema": "(1) Student\n- Attribute: student ID, name, age\n- Primary Key: student ID\n\n(2) Course\n- Attribute: course number, course name, credits, lecturer, class time\n- Primary Key: course number\n\n(3) StudentCourses\n- Attribute: student ID, course number\n- Primary Key: student ID, course number\n- Foreign Key: student ID (reference Student: student ID), course number (reference Course: course number)\n ",
"target_db_type": "mysql",
"model": "gpt4"
}
response = requests.post(url, json=data)
print(response.json())

@ -687,7 +687,11 @@ async def update_password_service(
raise exceptions.UserNotFoundException()
if not verify_password(password_data.old_password, db_user.password_hash):
raise exceptions.PasswordInvalidException(user_id=user_id, custom_message="旧密码不正确")
raise exceptions.PasswordInvalidException(user_id=user_id, custom_message="旧密码不正确", is_login=False)
# 校验新密码不能与旧密码相同
if password_data.old_password == password_data.new_password:
raise exceptions.ValidationException("新密码不能与旧密码相同")
new_hashed_password = get_password_hash(password_data.new_password)

@ -4,7 +4,7 @@
用于调试/验证 chat-to-SQL 流程连接可配置的 AI 端点并返回基础解析结果
"""
# backend/app/service/chat_service_test.py
# backend/app/service/test_chat_service.py
import json
import httpx

@ -239,7 +239,7 @@ const App: React.FC = () => {
const defaultRoute = isAdmin ? ROUTES.ADMIN_USERS : ROUTES.DASHBOARD;
return (
<div className="flex h-screen h-[100dvh] bg-[#f0f2f5] bg-[url('/background.svg')] bg-center bg-no-repeat bg-contain overflow-hidden">
<div className="flex h-screen h-[100dvh] min-w-[800px] min-h-[600px] bg-[#f0f2f5] bg-[url('/background.svg')] bg-center bg-no-repeat bg-contain">
<ToastContainer />
<Sidebar

@ -141,7 +141,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ role, user, selectedProject, a
*/}
<div className={`
fixed inset-y-0 left-0 w-64 bg-white border-r border-gray-200 h-screen h-[100dvh] flex flex-col
transition-[transform,width] duration-300 ease-in-out motion-reduce:transition-none z-50 overflow-x-hidden
transition-[transform,width] duration-300 ease-in-out motion-reduce:transition-none z-50
md:translate-x-0 md:static md:sticky md:top-0 md:h-screen md:h-[100dvh]
${isOpen ? 'translate-x-0' : '-translate-x-full'}
${isCollapsed ? 'md:w-16' : 'md:w-64'}

@ -16,7 +16,7 @@
* @date 2026-01-02
*/
import React, { JSX, ReactNode, useState, useEffect } from 'react';
import React, { JSX, ReactNode, useState, useEffect, useCallback } from 'react';
import { Check, X, CheckCircle, AlertCircle, Info, AlertTriangle, Eye, EyeOff } from 'lucide-react';
// =========================================================
@ -206,37 +206,53 @@ export const Select: React.FC<SelectProps> = ({
const displayText = selectedOption?.label || placeholder;
/**
*
* @description
* getBoundingClientRect()
*
*/
const handleToggle = () => {
if (disabled) return;
if (!isOpen && buttonRef.current) {
/** * Minimal
*
*/
let targetElement: HTMLElement | null = buttonRef.current;
if (variant === 'minimal') {
let parent = buttonRef.current.parentElement;
while (parent && parent !== document.body) {
const style = window.getComputedStyle(parent);
if (style.borderWidth && parseFloat(style.borderWidth) > 0) {
targetElement = parent;
break;
}
parent = parent.parentElement;
const updatePosition = useCallback(() => {
if (!isOpen || !buttonRef.current) return;
let targetElement: HTMLElement | null = buttonRef.current;
// Minimal 变体向上查找边框容器Default 变体直接基于按钮定位
if (variant === 'minimal') {
let parent = buttonRef.current.parentElement;
while (parent && parent !== document.body) {
const style = window.getComputedStyle(parent);
if (style.borderWidth && parseFloat(style.borderWidth) > 0) {
targetElement = parent;
break;
}
parent = parent.parentElement;
}
}
// 物理坐标重算
const rect = targetElement!.getBoundingClientRect();
setMenuPosition({
top: rect.bottom + 4,
left: rect.left,
width: rect.width
});
const rect = targetElement!.getBoundingClientRect();
setMenuPosition({
top: rect.bottom + 4,
left: rect.left,
width: rect.width
});
}, [isOpen, variant]);
/**
*
*/
useEffect(() => {
if (isOpen) {
updatePosition();
window.addEventListener('scroll', updatePosition, true);
window.addEventListener('resize', updatePosition);
}
return () => {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
};
}, [isOpen, updatePosition]);
/**
*
*/
const handleToggle = () => {
if (disabled) return;
setIsOpen(!isOpen);
};
@ -282,7 +298,7 @@ export const Select: React.FC<SelectProps> = ({
</svg>
</button>
{/* 下拉面板:采用 Fixed 布局及 Z-Index 策略确保不被层级覆盖 */}
{/* 下拉面板:采用 Fixed 布局及动态位置更新策略,确保不被遮挡且紧贴触发元素 */}
{isOpen && (
<div
className="fixed bg-white border border-gray-200 rounded-lg shadow-md overflow-hidden animate-in fade-in zoom-in-95 duration-150"
@ -294,7 +310,7 @@ export const Select: React.FC<SelectProps> = ({
}}
>
{/* 内部滚动区:最大高度限制为 5 个标准列表项的高度 */}
<div className="overflow-y-auto py-1" style={{ maxHeight: 'calc(5 * 44px)' }}>
<div className="overflow-y-scroll py-1" style={{ maxHeight: 'calc(5 * 44px)' }}>
{options.map((option) => (
<div
key={option.value}

@ -70,7 +70,7 @@ body {
padding: 0;
min-height: 100vh;
min-height: 100dvh;
overflow-x: hidden;
/* overflow-x: hidden; */
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@ -81,7 +81,7 @@ body {
min-height: 100dvh;
display: flex;
flex-direction: column;
overflow: hidden;
/* overflow: hidden; */
}
/* ========================================
@ -118,6 +118,7 @@ body {
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
@ -507,6 +508,7 @@ body {
text-overflow: ellipsis;
white-space: nowrap;
}
/* Hide default password reveal button */
input[type='password']::-ms-reveal,
input[type='password']::-ms-clear {
@ -516,4 +518,4 @@ input[type='password']::-ms-clear {
input[type='password']::-webkit-password-reveal-button {
display: none;
-webkit-appearance: none;
}
}

@ -74,6 +74,11 @@ export const AdminAIModels: React.FC = () => {
if (!['http:', 'https:'].includes(parsed.protocol)) {
return 'URL 协议必须是 http 或 https';
}
// 检查URL路径是否以 /v1/chat/completions 或 /chat/completions 结尾
const pathname = parsed.pathname.replace(/\/+$/, ''); // 去掉末尾斜杠
if (!pathname.endsWith('/v1/chat/completions') && !pathname.endsWith('/chat/completions')) {
return 'API 地址需以 /v1/chat/completions 或 /chat/completions 结尾';
}
} catch {
return 'URL 格式不正确,请检查';
}
@ -139,9 +144,16 @@ export const AdminAIModels: React.FC = () => {
return;
}
if (hasNewApiKey && !/^(sk-|ms-|ak-)[A-Za-z0-9\-]{10,}/i.test(formData.api_key.trim())) {
message.error('API 密钥格式不正确,需以 sk-/ms-/ak- 开头');
return;
if (hasNewApiKey) {
const key = formData.api_key.trim();
if (!/^(sk-|ms-|ak-)/i.test(key)) {
message.error('API 密钥格式不正确,需以 sk-/ms-/ak- 开头');
return;
}
if (!/^(sk-|ms-|ak-)[A-Za-z0-9\-]{10,}/i.test(key)) {
message.error('API 密钥长度不足,前缀后至少需要 10 个字符');
return;
}
}
if (!formData.model_id.trim()) {
@ -212,10 +224,14 @@ export const AdminAIModels: React.FC = () => {
// 如果输入了新密钥,校验格式
if (formData.api_key.trim()) {
const key = formData.api_key.trim();
if (!/^(sk-|ms-|ak-)[A-Za-z0-9\-]{10,}/i.test(key)) {
if (!/^(sk-|ms-|ak-)/i.test(key)) {
message.error('API 密钥格式不正确,需以 sk-/ms-/ak- 开头');
return;
}
if (!/^(sk-|ms-|ak-)[A-Za-z0-9\-]{10,}/i.test(key)) {
message.error('API 密钥长度不足,前缀后至少需要 10 个字符');
return;
}
}
if (!testResult || !testResult.success) {

@ -78,7 +78,7 @@ export const AdminStatus: React.FC = () => {
<Card className="bg-gradient-to-br from-purple-50 to-white border-purple-100">
<div className="flex items-center gap-3 mb-2 text-purple-600">
<Users size={20} />
<span className="font-bold"></span>
<span className="font-bold"></span>
</div>
<div className="text-2xl font-bold text-gray-800">{stats?.active_users_today ?? 0}</div>
<p className="text-xs text-purple-400 mt-1">Active Users Today</p>
@ -87,7 +87,7 @@ export const AdminStatus: React.FC = () => {
<Card className="bg-gradient-to-br from-green-50 to-white border-green-100">
<div className="flex items-center gap-3 mb-2 text-green-600">
<Zap size={20} />
<span className="font-bold"></span>
<span className="font-bold"></span>
</div>
<div className="text-2xl font-bold text-gray-800">{stats?.query_count_today ?? 0}</div>
<p className="text-xs text-green-600 mt-1">Queries Processed</p>

@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { ProjectDTO, fetchProjects, createProject, updateProject, confirmDeleteProject, deleteProject, getProjectDetail } from '../api/project';
import { ProjectStatusEnum } from '../types';
import { ProjectStatusEnum, BusinessError } from '../types';
import { Button, Modal, Input, message } from '../components/UI';
import { Plus, PlayCircle, Sparkles, AlertTriangle, LayoutDashboard, Loader2 } from 'lucide-react';
import { ProjectWizard } from '../components/ProjectWizard';
@ -90,10 +90,18 @@ export const Dashboard: React.FC<DashboardProps> = ({ onProjectSelect }) => {
message.error('请输入项目名称');
return;
}
if (newProjectName.trim().length > 50) {
message.error('项目名称不能超过50个字符');
return;
}
if (!newProjectDesc.trim()) {
message.error('请输入业务场景描述');
return;
}
if (newProjectDesc.trim().length > 1000) {
message.error('业务场景描述不能超过1000个字符');
return;
}
if (isCreating) return;
setIsCreating(true);
@ -111,7 +119,10 @@ export const Dashboard: React.FC<DashboardProps> = ({ onProjectSelect }) => {
setIsDeploying(false);
setIsCreating(false);
setCurrentProjectId(null);
message.error("创建失败,请检查网络或重试");
// 如果是业务错误(如配额超出),已由全局错误处理器显示,不再重复提示
if (!(e instanceof BusinessError)) {
message.error("创建失败,请检查网络或重试");
}
}
};
@ -299,7 +310,10 @@ export const Dashboard: React.FC<DashboardProps> = ({ onProjectSelect }) => {
</div>
</div>
<div className="space-y-6">
<Input label="项目名称" placeholder="例如:企业级 CRM 客户管理系统" value={newProjectName} onChange={(e) => setNewProjectName(e.target.value)} required />
<div className="flex flex-col gap-2">
<Input label="项目名称" placeholder="例如:企业级 CRM 客户管理系统" value={newProjectName} onChange={(e) => setNewProjectName(e.target.value)} maxLength={50} required />
<span className="text-xs text-gray-400 text-right">{newProjectName.length}/50</span>
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-gray-700"><span className="text-red-500 ml-1">*</span></label>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 sm:gap-4">
@ -322,7 +336,8 @@ export const Dashboard: React.FC<DashboardProps> = ({ onProjectSelect }) => {
</div>
<div className="flex flex-col gap-2">
<label className="text-sm font-medium text-gray-700"><span className="text-red-500 ml-1">*</span></label>
<textarea className="px-4 py-3 bg-white border border-gray-300 rounded-lg text-sm focus:border-primary focus:ring-2 focus:ring-blue-100 transition-all h-32 resize-none leading-relaxed" placeholder="请详细描述实体及关系..." value={newProjectDesc} onChange={(e) => setNewProjectDesc(e.target.value)}></textarea>
<textarea className="px-4 py-3 bg-white border border-gray-300 rounded-lg text-sm focus:border-primary focus:ring-2 focus:ring-blue-100 transition-all h-32 resize-none leading-relaxed" placeholder="请详细描述实体及关系..." value={newProjectDesc} onChange={(e) => setNewProjectDesc(e.target.value)} maxLength={1000}></textarea>
<span className="text-xs text-gray-400 text-right">{newProjectDesc.length}/1000</span>
</div>
</div>
</div>

@ -397,13 +397,6 @@ export const Profile: React.FC<ProfileProps> = ({ onLogout, onUpdateUser }) => {
{userProfile?.created_at ? new Date(userProfile.created_at).toLocaleDateString() : '-'}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-500"></span>
{/* 修复:对 userProfile.last_login_at 进行存在性检查 */}
<span className="text-gray-800">
{userProfile?.last_login_at ? new Date(userProfile.last_login_at).toLocaleString() : '从未登录'}
</span>
</div>
</div>
</Card>
</div>
@ -576,7 +569,7 @@ export const Profile: React.FC<ProfileProps> = ({ onLogout, onUpdateUser }) => {
<td className="px-4 py-3 text-right">
<Tag color={log.login_status === 'failed' ? 'red' : 'green'}>
{log.login_status === 'success' ? '成功' :
log.login_status === 'forced_logout' ? '登出' :
log.login_status === 'forced_logout' ? '登出' :
log.login_status === 'expired' ? '已过期' : '失败'}
</Tag>
</td>

@ -177,10 +177,10 @@ export const UserProfile: React.FC<UserProfileProps> = ({ user, onBack }) => {
{statusLabel}
</Tag>
</h2>
<div className="text-gray-500 mt-2 flex items-center gap-6 text-sm">
<span className="flex items-center gap-1"><Shield size={14} /> ID: {user.id}</span>
<span className="flex items-center gap-1">📧 {user.email}</span>
<span className="flex items-center gap-1"><Clock size={14} /> : {lastLoginText}</span>
<div className="text-gray-500 mt-2 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm">
<span className="flex items-center gap-1 shrink-0 whitespace-nowrap"><Shield size={14} /> ID: {user.id}</span>
<span className="flex items-center gap-1 shrink-0 whitespace-nowrap">📧 {user.email}</span>
<span className="flex items-center gap-1 shrink-0 whitespace-nowrap"><Clock size={14} /> : {lastLoginText}</span>
</div>
</div>
</div>
@ -228,17 +228,17 @@ export const UserProfile: React.FC<UserProfileProps> = ({ user, onBack }) => {
return (
<div key={log.login_id} className="flex items-start gap-3 text-sm border-b border-gray-50 pb-3 last:border-0 last:pb-0">
<div className={`w-2 h-2 mt-1.5 rounded-full ${isFailed ? 'bg-red-500' : 'bg-green-500'}`}></div>
<div className="flex-1">
<div className="flex justify-between">
<span className="font-medium text-gray-700">{log.ip_address}</span>
<span className="text-gray-400 text-xs">{timeText}</span>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center gap-2">
<span className="font-medium text-gray-700 truncate">{log.ip_address}</span>
<span className="text-gray-400 text-xs whitespace-nowrap">{timeText}</span>
</div>
<div className="flex items-center gap-2 text-gray-500 text-xs mt-1">
<span className="flex items-center gap-0.5"><MapPin size={10} /> {log.login_status === 'success' ? '成功' :
log.login_status === 'forced_logout' ? '登出' :
<div className="flex items-center gap-2 text-gray-500 text-xs mt-1 w-full overflow-hidden">
<span className="flex items-center gap-0.5 whitespace-nowrap flex-shrink-0"><MapPin size={10} /> {log.login_status === 'success' ? '成功' :
log.login_status === 'forced_logout' ? '登出' :
log.login_status === 'expired' ? '已过期' : '失败'}</span>
<span className="flex items-center gap-0.5 max-w-[220px] truncate" title={log.user_agent || 'Unknown'}>
<Globe size={10} /> {log.user_agent || 'Unknown'}
<span className="flex items-center gap-0.5 flex-1 min-w-0" title={log.user_agent || 'Unknown'}>
<Globe size={10} className="flex-shrink-0" /> <span className="truncate">{log.user_agent || 'Unknown'}</span>
</span>
</div>
</div>

@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Project, Message, QueryResult, ChatSession, ChatResponse, AIModelOption } from '../types';
import { Button, message as GlobalMessage, Modal, ConfirmDialog } from '../components/UI';
import { Send, Plus, MessageSquare, Edit2, Trash2, Check, X, ChevronLeft, Loader2, Sparkles, AlertTriangle, Play, Ban, Table as TableIcon, Info, Bot, Database } from 'lucide-react';
@ -62,6 +62,29 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ value, onChange, options,
const selectorRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
// 动态位置更新
const updatePosition = useCallback(() => {
if (!isOpen || !buttonRef.current) return;
const rect = buttonRef.current.getBoundingClientRect();
setMenuPosition({
bottom: window.innerHeight - rect.top + 4,
left: rect.left
});
}, [isOpen]);
// 监听滚动和调整大小
useEffect(() => {
if (isOpen) {
updatePosition();
window.addEventListener('scroll', updatePosition, true);
window.addEventListener('resize', updatePosition);
}
return () => {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
};
}, [isOpen, updatePosition]);
// 点击外部关闭下拉菜单
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
@ -73,15 +96,8 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ value, onChange, options,
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// 计算下拉菜单位置
// 切换开关
const handleToggle = () => {
if (!isOpen && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
setMenuPosition({
bottom: window.innerHeight - rect.top + 4,
left: rect.left
});
}
setIsOpen(!isOpen);
};
@ -120,7 +136,7 @@ const ModelSelector: React.FC<ModelSelectorProps> = ({ value, onChange, options,
left: menuPosition.left
}}
>
<div className="overflow-y-auto py-1" style={{ maxHeight: 'calc(5 * 44px)' }}>
<div className="overflow-y-scroll py-1" style={{ maxHeight: 'calc(5 * 44px)' }}>
{options.map((option) => (
<div
key={option.value}

@ -72,7 +72,7 @@ export function calculateGlobalScaleFactor(zoomLevel: number): number {
* 2
* power < 1 (0.7) 70% 30%
*/
const power = 0.7;
const power = 0.7;
const rawFactor = Math.pow(100 / zoomLevel, power);
/**
@ -112,8 +112,8 @@ function calculateGlobalZoomLevel(): GlobalZoomLevelInfo {
* A>= 2.5K DPR 2
*/
if (screenWidth >= 2560 || screenHeight >= 1440) {
baseDPR = 2;
}
baseDPR = 2;
}
/**
* B1080P 1
*/

Loading…
Cancel
Save