From 8a12ad04db057ba5d805f60b56cf4b94cbb6b32c Mon Sep 17 00:00:00 2001 From: dark Date: Wed, 4 Feb 2026 16:11:13 +0800 Subject: [PATCH] Initial commit: Add project structure and .gitignore --- .claude/settings.local.json | 31 + .claude/skills/zero-skills | 1 + .gitignore | 111 +++ AI-TOOLS-INTEGRATION.md | 707 ++++++++++++++++++ backend/ai-context.md | 187 +++++ backend/api/profile.api | 29 + backend/api/user.api | 50 ++ backend/base.api | 125 ++++ backend/base.go | 34 + backend/etc/base-api.yaml | 7 + backend/go-zero.md | 58 ++ backend/go.mod | 59 ++ backend/go.sum | 146 ++++ backend/internal/config/config.go | 15 + backend/internal/handler/auth/loginhandler.go | 32 + .../handler/auth/refreshtokenhandler.go | 32 + .../internal/handler/auth/registerhandler.go | 32 + .../handler/profile/changepasswordhandler.go | 32 + .../handler/profile/getprofilehandler.go | 25 + .../handler/profile/updateprofilehandler.go | 32 + backend/internal/handler/routes.go | 110 +++ .../handler/user/createuserhandler.go | 32 + .../handler/user/deleteuserhandler.go | 32 + .../internal/handler/user/getuserhandler.go | 32 + .../handler/user/getuserlisthandler.go | 32 + .../handler/user/updateuserhandler.go | 32 + backend/internal/logic/auth/loginlogic.go | 69 ++ .../internal/logic/auth/refreshtokenlogic.go | 68 ++ backend/internal/logic/auth/registerlogic.go | 77 ++ .../logic/profile/changepasswordlogic.go | 73 ++ .../internal/logic/profile/getprofilelogic.go | 76 ++ .../logic/profile/updateprofilelogic.go | 121 +++ .../internal/logic/user/createuserlogic.go | 76 ++ .../internal/logic/user/deleteuserlogic.go | 56 ++ .../internal/logic/user/getuserlistlogic.go | 57 ++ backend/internal/logic/user/getuserlogic.go | 51 ++ .../internal/logic/user/updateuserlogic.go | 81 ++ backend/internal/middleware/authmiddleware.go | 48 ++ backend/internal/middleware/corsmiddleware.go | 22 + backend/internal/middleware/logmiddleware.go | 22 + backend/internal/svc/servicecontext.go | 59 ++ backend/internal/types/types.go | 103 +++ backend/internal/util/jwt/jwt.go | 59 ++ backend/mcp-zero.md | 60 ++ backend/model/profile_entity.go | 20 + backend/model/profile_model.go | 52 ++ backend/model/user_entity.go | 22 + backend/model/user_model.go | 94 +++ backend/tests/USER_MODULE_TEST_STANDARD.md | 333 +++++++++ backend/tests/profile/test_profile.sh | 193 +++++ backend/tests/profile/test_profile_no_jq.sh | 207 +++++ backend/tests/run_all_tests.sh | 121 +++ backend/tests/user/test_user.sh | 136 ++++ backend/zero-skills.md | 122 +++ frontend/react-native/ai-context.md | 257 +++++++ frontend/react-shadcn/llms.txt | 149 ++++ frontend/vue-primevue/llms.txt | 234 ++++++ readme.md | 43 ++ 58 files changed, 5176 insertions(+) create mode 100644 .claude/settings.local.json create mode 160000 .claude/skills/zero-skills create mode 100644 .gitignore create mode 100644 AI-TOOLS-INTEGRATION.md create mode 100644 backend/ai-context.md create mode 100644 backend/api/profile.api create mode 100644 backend/api/user.api create mode 100644 backend/base.api create mode 100644 backend/base.go create mode 100644 backend/etc/base-api.yaml create mode 100644 backend/go-zero.md create mode 100644 backend/go.mod create mode 100644 backend/go.sum create mode 100644 backend/internal/config/config.go create mode 100644 backend/internal/handler/auth/loginhandler.go create mode 100644 backend/internal/handler/auth/refreshtokenhandler.go create mode 100644 backend/internal/handler/auth/registerhandler.go create mode 100644 backend/internal/handler/profile/changepasswordhandler.go create mode 100644 backend/internal/handler/profile/getprofilehandler.go create mode 100644 backend/internal/handler/profile/updateprofilehandler.go create mode 100644 backend/internal/handler/routes.go create mode 100644 backend/internal/handler/user/createuserhandler.go create mode 100644 backend/internal/handler/user/deleteuserhandler.go create mode 100644 backend/internal/handler/user/getuserhandler.go create mode 100644 backend/internal/handler/user/getuserlisthandler.go create mode 100644 backend/internal/handler/user/updateuserhandler.go create mode 100644 backend/internal/logic/auth/loginlogic.go create mode 100644 backend/internal/logic/auth/refreshtokenlogic.go create mode 100644 backend/internal/logic/auth/registerlogic.go create mode 100644 backend/internal/logic/profile/changepasswordlogic.go create mode 100644 backend/internal/logic/profile/getprofilelogic.go create mode 100644 backend/internal/logic/profile/updateprofilelogic.go create mode 100644 backend/internal/logic/user/createuserlogic.go create mode 100644 backend/internal/logic/user/deleteuserlogic.go create mode 100644 backend/internal/logic/user/getuserlistlogic.go create mode 100644 backend/internal/logic/user/getuserlogic.go create mode 100644 backend/internal/logic/user/updateuserlogic.go create mode 100644 backend/internal/middleware/authmiddleware.go create mode 100644 backend/internal/middleware/corsmiddleware.go create mode 100644 backend/internal/middleware/logmiddleware.go create mode 100644 backend/internal/svc/servicecontext.go create mode 100644 backend/internal/types/types.go create mode 100644 backend/internal/util/jwt/jwt.go create mode 100644 backend/mcp-zero.md create mode 100644 backend/model/profile_entity.go create mode 100644 backend/model/profile_model.go create mode 100644 backend/model/user_entity.go create mode 100644 backend/model/user_model.go create mode 100644 backend/tests/USER_MODULE_TEST_STANDARD.md create mode 100644 backend/tests/profile/test_profile.sh create mode 100644 backend/tests/profile/test_profile_no_jq.sh create mode 100644 backend/tests/run_all_tests.sh create mode 100644 backend/tests/user/test_user.sh create mode 100644 backend/zero-skills.md create mode 100644 frontend/react-native/ai-context.md create mode 100644 frontend/react-shadcn/llms.txt create mode 100644 frontend/vue-primevue/llms.txt create mode 100644 readme.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..b7b5379 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,31 @@ +{ + "permissions": { + "allow": [ + "Bash(powershell:*)", + "Bash(tree:*)", + "Bash(go get:*)", + "Bash(go mod init:*)", + "Bash(go mod tidy:*)", + "Bash(ls:*)", + "Bash(go run:*)", + "Bash(go list:*)", + "Bash(go mod download:*)", + "Bash(git clone:*)", + "Bash(goctl:*)", + "Bash(go install:*)", + "Bash(~/go/bin/goctl version:*)", + "Bash(~/go/bin/goctl.exe:*)", + "Bash(~/go/bin/goctl.exe -v)", + "Bash(go build:*)", + "Bash(mysql:*)", + "Bash(claude:*)", + "Bash(npm view:*)", + "Bash(./user.exe:*)", + "Bash(timeout:*)", + "Bash(tasklist:*)", + "Bash(dir:*)", + "Bash(findstr:*)", + "Bash(netstat:*)" + ] + } +} diff --git a/.claude/skills/zero-skills b/.claude/skills/zero-skills new file mode 160000 index 0000000..789a422 --- /dev/null +++ b/.claude/skills/zero-skills @@ -0,0 +1 @@ +Subproject commit 789a42248d8a81cec421a2bb4a1f2db7da63d65c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80178be --- /dev/null +++ b/.gitignore @@ -0,0 +1,111 @@ +# Go +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +go.work +vendor/ + +# Node.js / Web +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +dist/ +build/ +.next/ +.nuxt/ +.cache/ +.parcel-cache/ + +# React Native +android/app/build/ +android/build/ +ios/build/ +*.ipa +*.apk +.expo/ +.expo-shared/ +metro.config.js.map + +# General +.DS_Store +Thumbs.db +*.log +*.tmp +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Logs +logs/ +*.log + +# Runtime data +pids/ +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Temporary folders +tmp/ +temp/ \ No newline at end of file diff --git a/AI-TOOLS-INTEGRATION.md b/AI-TOOLS-INTEGRATION.md new file mode 100644 index 0000000..60ea184 --- /dev/null +++ b/AI-TOOLS-INTEGRATION.md @@ -0,0 +1,707 @@ +# Go-Zero AI 工具集成指南 + +本文档详细说明 `zero-skills`、`ai-context` 和 `mcp-zero` 三个工具如何配合使用。 + +--- + +## 📊 三件套概览 + +| 工具 | 用途 | 大小 | 最适合工具 | 角色 | +|------|------|------|-----------|------| +| **ai-context** | 工作流指令和决策树 | ~5KB | GitHub Copilot, Cursor, Windsurf | 工作流层 | +| **zero-skills** | 完整知识库 | ~40KB | Claude Code,深度学习 | 知识层 | +| **mcp-zero** | 运行时工具(goctl 命令) | MCP Server | Claude Desktop/Code | 执行层 | + +--- + +## 🎯 架构协作图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ AI 助手 │ +│ (Claude Code, GitHub Copilot, Cursor, 等) │ +└────────────┬─────────────────────┬──────────────────────────┘ + │ │ + ├─ 工作流层 ──────────┤ + │ ai-context │ "做什么" - 快速决策 + │ (~5KB) │ 每次交互都加载 + │ │ + ├─ 知识层 ────────────┤ + │ zero-skills │ "如何和为什么" - 详细模式 + │ (~40KB) │ 需要时加载 + │ │ + └─ 执行层 ────────────┘ + mcp-zero "执行" - 运行 goctl 命令 + (MCP Server) 生成实际代码文件 +``` + +--- + +## 📦 工具详细说明 + +### 1. ai-context - 工作流层 + +**定位**: 快速决策工具,回答"做什么" + +**功能**: +- 提供简洁的工作流指导 +- 帮助 AI 快速做出开发决策 +- 每次交互都加载,轻量级(~5KB) + +**适用场景**: +- 快速生成代码片段 +- 内联代码补全 +- 需要快速决策的开发流程 + +**安装方式**: +```bash +# GitHub Copilot +git clone https://github.com/zeromicro/ai-context.git .github/copilot-instructions.md + +# Cursor +git clone https://github.com/zeromicro/ai-context.git .cursorrules + +# Windsurf +git clone https://github.com/zeromicro/ai-context.git .windsurfrules +``` + +**使用特点**: +- ⚡ 快速加载 +- 📝 简洁的决策树 +- 🎯 适合日常开发 + +--- + +### 2. zero-skills - 知识层 + +**定位**: 深度知识库,回答"如何和为什么" + +**功能**: +- 完整的 go-zero 模式指南 +- 三层架构(Handler → Logic → Model)详解 +- 正确和错误示例对比 +- 生产级最佳实践 + +**适用场景**: +- 深度学习和理解 go-zero +- 复杂的微服务架构设计 +- 代码审查和问题排查 +- 需要 AI Code 的高级功能 + +**安装方式**: +```bash +# 项目级别(推荐) +git clone https://github.com/zeromicro/zero-skills.git .claude/skills/zero-skills + +# 个人级别 +git clone https://github.com/zeromicro/zero-skills.git ~/.claude/skills/zero-skills +``` + +**在 Claude Code 中的使用**: +```bash +# 自动加载 +# 处理 go-zero 文件(.api, .proto, go.mod)时自动加载 + +# 手动调用 +/zero-skills + +# 带参数调用 +/zero-skills 创建用户管理 API + +# 检查可用性 +What skills are available? +``` + +**使用特点**: +- 📚 完整知识库(~40KB) +- 🎓 需要时加载,按需获取 +- 🔍 详细模式和示例 +- 🤖 支持子代理和动态上下文 + +--- + +### 3. mcp-zero - 执行层 + +**定位**: 代码执行工具,负责"执行" + +**功能**: +- 执行 `goctl` 命令生成代码 +- 生成实际的项目文件 +- MCP Server 协议集成 + +**适用场景**: +- 自动化代码生成 +- 批量创建服务文件 +- Claude Desktop/Code 环境使用 + +**安装方式**: + +1. 安装 MCP Server: +```bash +npm install -g @zeromicro/mcp-zero +``` + +2. 配置 Claude Desktop: +```json +{ + "mcpServers": { + "mcp-zero": { + "command": "node", + "args": ["C:\\Users\\YourUser\\AppData\\Roaming\\npm\\node_modules\\@zeromicro\\mcp-zero\\dist\\index.js"], + "env": { + "GOCTL_PATH": "/path/to/goctl" + } + } + } +} +``` + +**使用特点**: +- ⚙️ 执行 goctl 命令 +- 📁 生成实际代码文件 +- 🔌 MCP 协议集成 + +--- + +## 🚀 使用场景 + +### 场景 1: GitHub Copilot 用户 + +**工具组合**: 仅 `ai-context` + +**工作流程**: +``` +用户编辑代码 + ↓ +加载 ai-context + ↓ +AI 提供快速建议 + ↓ +用户手动运行 goctl 命令 +``` + +**优点**: +- 快速内联建议 +- 工作流指导 +- 无需额外配置 + +**限制**: +- 无代码执行能力 +- 需手动运行 goctl 命令 + +**示例使用**: +``` +# .github/copilot-instructions.md + + +当编辑 go-zero 文件时,遵循以下流程: +1. 先定义 .api 文件类型和路由 +2. 使用 goctl 生成代码 +3. 在 internal/logic/ 实现业务逻辑 +4. 使用 sqlx 进行数据库操作 +``` + +--- + +### 场景 2: Claude Code 用户(最佳体验)⭐ + +**工具组合**: `zero-skills` + `mcp-zero` + +**工作流程**: +``` +用户发起请求 + ↓ +加载 zero-skills (知识层) + ↓ +AI 调用 mcp-zero 工具 (执行层) + ↓ +执行 goctl 命令生成代码 + ↓ +返回生成的文件和说明 +``` + +**优点**: +- 来自模式指南的深度知识 +- 通过 goctl 自动生成代码 +- 实时项目数据的动态上下文 +- 复杂任务的子代理工作流 + +**完整示例**: + +用户: `/zero-skills 创建用户管理 API` + +AI 处理: +1. 加载 `zero-skills` 的 `rest-api-patterns.md` +2. 了解三层架构要求 +3. 调用 `mcp-zero` 的 `generate_api` 工具: + ```bash + goctl api go -api user.api -dir . + ``` +4. 生成文件结构: + ``` + . + ├── user.api + ├── etc/ + ├── internal/ + │ ├── handler/ + │ ├── logic/ + │ ├── svc/ + │ └── types/ + └── main.go + ``` +5. 解释生成的代码结构 + +**高级功能**: +```markdown +# 子代理使用示例 +/zero-skills 分析项目架构 + +AI 将: +1. Fork 子代理 +2. 只读分析项目文件 +3. 返回架构分析报告 +``` + +--- + +### 场景 3: Cursor/Windsurf 用户 + +**工具组合**: `ai-context` + `zero-skills` 链接 + +**工作流程**: +``` +用户在 IDE 中编码 + ↓ +加载 ai-context (快速决策) + ↓ +必要时链接到 zero-skills (深度知识) + ↓ +AI 提供建议和指导 +``` + +**优点**: +- IDE 原生体验 +- 快速建议 + 深度知识 +- 灵活使用 + +**配置示例**: + +`.cursorrules`: +```markdown + +当使用 go-zero 框架时: +- 遵循三层架构: Handler → Logic → Model +- 使用 goctl 生成代码 +- 使用 httpx.Error() 处理错误 + + +详情参考: .claude/skills/zero-skills/references/rest-api-patterns.md +``` + +--- + +## 🔗 工具间的协作示例 + +### 示例 1: 创建完整的 REST API + +**用户请求**: "创建用户管理的 REST API" + +**三件套协作**: + +``` +Step 1: ai-context 决策 (工作流层) +------------------------------------ +决定创建流程: +1. 定义 .api 文件 +2. 生成代码 +3. 实现业务逻辑 + +Step 2: zero-skills 指导 (知识层) +------------------------------------ +生成 user.api 内容参考: +- 类型定义结构 +- 路由定义规范 +- handler 命名约定 +- 验证规则 + +Step 3: mcp-zero 执行 (执行层) +------------------------------------ +执行命令: +goctl api go -api user.api -dir . + +生成文件: +├── user.api +├── etc/user-api.yaml +├── internal/handler/*.go +├── internal/logic/*.go +├── internal/svc/servicecontext.go +├── internal/types/types.go +└── user.go +``` + +--- + +### 示例 2: 修复数据库连接问题 + +**用户请求**: "数据库连接失败,怎么排查" + +**三件套协作**: + +``` +Step 1: ai-context 决策 +------------------------------------ +分析可能的错误原因: +- 连接字符串错误 +- 数据库服务未启动 +- 防火墙阻止 +- 驱动问题 + +Step 2: zero-skills 深度分析 +------------------------------------ +从 database-patterns.md 获取: +- 正确的连接字符串格式 +- ServiceContext 注入方法 +- sqlx 配置方式 +- 错误处理最佳实践 + +Step 3: 提供解决方案 +------------------------------------ +参考配置: +```go +// internal/svc/servicecontext.go +type ServiceContext struct { + Config config.Config + SqlConn sqlx.SqlConn +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + SqlConn: sqlx.NewMysql(c.Database.DataSource), + } +} +``` + +检查清单: +- [ ] 数据库服务运行状态 +- [ ] 连接字符串格式 +- [ ] 防火墙设置 +- [ ] sqlx 导入正确 +``` + +--- + +## 📋 快速参考 + +### 工具选择决策树 + +``` +开始 + │ + ├─ 使用什么 AI 工具? + │ ├─ GitHub Copilot → 使用 ai-context + │ ├─ Cursor/Windsurf → 使用 ai-context + zero-skills + │ └─ Claude Code → 使用 zero-skills + mcp-zero + │ + └─ 任务类型? + ├─ 快速代码片段 → ai-context + ├─ 深度学习/参考 → zero-skills + └─ 生成项目文件 → mcp-zero +``` + +### 命令速查 + +```bash +# ai-context 安装 +git clone https://github.com/zeromicro/ai-context.git .github/copilot-instructions.md + +# zero-skills 安装(项目级) +git clone https://github.com/zeromicro/zero-skills.git .claude/skills/zero-skills + +# zero-skills 安装(个人级) +git clone https://github.com/zeromicro/zero-skills.git ~/.claude/skills/zero-skills + +# mcp-zero 安装 +npm install -g @zeromicro/mcp-zero + +# 手动调用 zero-skills +/zero-skills +/zero-skills 创建用户管理 API +``` + +--- + +## 💡 最佳实践 + +### 1. Claude Code 环境推荐配置 + +``` +项目级 zero-skills + mcp-zero +``` + +**为什么**: +- 自动加载,无需手动触发 +- 完整知识库 + 代码执行 +- 支持高级功能(子代理、动态上下文) + +### 2. 多工具环境配置 + +``` +~/.claude/skills/zero-skills # 所有项目可用 +~/.claude/skills/ai-context # 可选 +项目/.github/copilot-instructions.md # Copilot 专用 +项目/.cursorrules # Cursor 专用 +``` + +### 3. 性能优化 + +- **轻量级需求**: 仅用 ai-context(5KB) +- **深度学习**: zero-skills(40KB,按需加载) +- **代码生成**: 配合 mcp-zero 执行层 + +--- + +## 🔗 相关资源 + +- **[ai-context GitHub](https://github.com/zeromicro/ai-context)** - 工作流层 +- **[zero-skills GitHub](https://github.com/zeromicro/zero-skills)** - 知识层 +- **[mcp-zero GitHub](https://github.com/zeromicro/mcp-zero)** - 执行层 +- **[go-zero 官方文档](https://go-zero.dev)** - 框架文档 +- **[Agent Skills 规范](https://agentskills.io/)** - 技能规范 + +--- + +## 📝 总结 + +| 层级 | 工具 | 作用 | 大小 | +|------|------|------|------| +| 工作流层 | ai-context | 快速决策"做什么" | ~5KB | +| 知识层 | zero-skills | 深度知识"如何和为什么" | ~40KB | +| 执行层 | mcp-zero | 执行代码生成 | MCP Server | + +**核心思想**: 根据不同的 AI 工具和任务需求,灵活组合三件套,实现最高效的开发体验。 + +--- + +## 📦 Go-Zero + GORM 模块开发实战指南 + +### 概述 + +使用 GORM 替代默认的 sqlx 时,goctl 生成的代码需要手动修复多处问题。本文档记录了用户模块开发过程中遇到的坑点和解决方案。 + +--- + +### 一、生成基础代码 + +```bash +# 1. 生成 API 定义 +goctl api go -api user.api -dir backend + +# 2. 生成 model 代码(如果使用 sqlx) +goctl model mysql ddl -src user.sql -dir backend/model +``` + +**注意:** 使用 GORM 时,`goctl model` 生成的代码可能不适用,需要手动定义 Model 和 CRUD 方法。 + +--- + +### 二、修复 ServiceContext (GORM 初始化) + +文件:`backend/internal/svc/servicecontext.go` + +#### 常见错误 1:gorm.Open 参数不足 + +❌ 错误: +```go +db, err := gorm.Open(mysql.Open(dsn)) +``` + +✅ 正确: +```go +db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) +``` + +#### 常见错误 2:DB.Close() 方法不存在 + +❌ 错误: +```go +func (s *ServiceContext) Close() error { + if s.DB != nil { + return s.DB.Close() // *gorm.DB 没有 Close 方法 + } + return nil +} +``` + +✅ 正确: +```go +func (s *ServiceContext) Close() error { + if s.DB != nil { + sqlDB, err := s.DB.DB() // 获取底层 *sql.DB + if err != nil { + return err + } + return sqlDB.Close() + } + return nil +} +``` + +#### 完整的 NewServiceContext + +```go +func NewServiceContext(c config.Config) *ServiceContext { + dsn := "user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=true&loc=Local" + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + panic("Failed to connect database: " + err.Error()) + } + + // 自动迁移(必须在启动时调用) + err = db.AutoMigrate(&model.User{}) + if err != nil { + panic("Failed to migrate database: " + err.Error()) + } + + return &ServiceContext{ + Config: c, + DB: db, + // ...其他字段 + } +} +``` + +--- + +### 三、实现 Model 层 (CRUD 操作) + +**关键点:** `goctl` 生成的 Logic 文件会引用 `model.FindOne`、`model.Insert` 等方法,但默认的 `model` 包只包含实体定义,**缺少这些 CRUD 方法**。 + +需要手动创建:`backend/model/_model.go` + +#### 标准模板 + +```go +package model + +import ( + "context" + "errors" + "gorm.io/gorm" +) + +var ( + ErrNotFound = errors.New("record not found") +) + +// Insert 插入记录 +func Insert(ctx context.Context, db *gorm.DB, record interface{}) (int64, error) { + result := db.WithContext(ctx).Create(record) + if result.Error != nil { + return 0, result.Error + } + return record.(*User).Id, nil // 根据实际结构体调整 +} + +// FindOne 根据ID查询 +func FindOne(ctx context.Context, db *gorm.DB, id int64) (*User, error) { + var user User + result := db.WithContext(ctx).First(&user, id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, result.Error + } + return &user, nil +} + +// FindOneByXxx 根据唯一字段查询 +func FindOneByEmail(ctx context.Context, db *gorm.DB, email string) (*User, error) { + var user User + result := db.WithContext(ctx).Where("email = ?", email).First(&user) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, result.Error + } + return &user, nil +} + +// Update 更新记录 +func Update(ctx context.Context, db *gorm.DB, user *User) error { + return db.WithContext(ctx).Save(user).Error +} + +// Delete 删除记录 +func Delete(ctx context.Context, db *gorm.DB, id int64) error { + return db.WithContext(ctx).Delete(&User{}, id).Error +} + +// FindList 分页查询列表 +func FindList(ctx context.Context, db *gorm.DB, page, pageSize int64, keyword string, status int64) ([]User, int64, error) { + var users []User + var total int64 + + query := db.WithContext(ctx).Model(&User{}) + + // 关键字搜索 + if keyword != "" { + query = query.Where("username LIKE ? OR email LIKE ?", + "%"+keyword+"%", "%"+keyword+"%") + } + + // 状态筛选 + if status > 0 { + query = query.Where("status = ?", status) + } + + // 统计总数 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 分页 + offset := (page - 1) * pageSize + if offset < 0 { + offset = 0 + } + + err := query.Offset(int(offset)).Limit(int(pageSize)).Find(&users).Error + if err != nil { + return nil, 0, err + } + + return users, total, nil +} +``` + +--- + +### 四、开发清单 + +- [ ] 修复 `gorm.Open` 调用,添加 `&gorm.Config{}` +- [ ] 修复 `Close()` 方法,使用 `db.DB().Close()` +- [ ] 在 `NewServiceContext` 中添加 `AutoMigrate` +- [ ] 为每个模块手动创建 `_model.go` +- [ ] 实现 `Insert`, `FindOne`, `Update`, `Delete`, `FindList` +- [ ] 定义 `ErrNotFound` 用于逻辑层判断 + +--- + +### 五、参考文件结构 + +``` +backend/ +├── model/ +│ ├── user_entity.go # 实体定义 +│ └── user_model.go # CRUD 方法 (手动添加) +├── internal/ +│ └── svc/ +│ └── servicecontext.go # GORM 初始化 +└── internal/logic/user/ + ├── createuserlogic.go # 引用 model.Insert + ├── getuserlogic.go # 引用 model.FindOne + └── ... +``` diff --git a/backend/ai-context.md b/backend/ai-context.md new file mode 100644 index 0000000..0e9a476 --- /dev/null +++ b/backend/ai-context.md @@ -0,0 +1,187 @@ +# AI Context - Go 上下文管理 + +> ai-context 是 Go 语言的 AI 上下文管理库,用于管理 AI 对话的上下文和状态。 + +## 官方资源 + +- [GitHub](https://github.com/zeromicro/ai-context) +- [文档](https://github.com/zeromicro/ai-context?tab=readme-ov-file) + +## 概述 + +ai-context 提供了一套完整的 AI 上下文管理解决方案,支持对话历史、状态管理和上下文压缩等功能。 + +## 核心功能 + +### 对话历史管理 +- 存储和管理完整的对话历史 +- 支持多轮对话追踪 +- 消息角色识别(用户、助手、系统) + +### 上下文压缩 +- 智能摘要生成 +- 上下文窗口优化 +- 历史消息归档 + +### 状态管理 +- 会话状态持久化 +- 分布式存储支持 +- 状态恢复机制 + +## 基本使用 + +```go +package main + +import ( + "context" + "github.com/zeromicro/ai-context" +) + +func main() { + // 创建上下文管理器 + manager := context.NewManager(context.Config{ + Storage: "memory", // 或 "redis" + MaxSize: 100, // 最大历史消息数 + }) + + // 创建会话 + sessionID := "session-123" + ctx := manager.CreateSession(sessionID) + + // 添加消息 + ctx.AddMessage(context.Message{ + Role: "user", + Content: "你好,今天天气怎么样?", + }) + + ctx.AddMessage(context.Message{ + Role: "assistant", + Content: "今天天气晴朗,温度适宜。", + }) + + // 获取历史消息 + history := ctx.GetHistory() + for _, msg := range history { + fmt.Printf("%s: %s\n", msg.Role, msg.Content) + } + + // 获取用于 AI 调用的上下文 + aiContext := ctx.GetContextForAI() + // 使用 aiContext 调用 AI 模型... + + // 保存会话状态 + manager.SaveSession(sessionID) +} +``` + +## 配置选项 + +```go +type Config struct { + Storage string // 存储类型: memory, redis, database + MaxSize int // 最大消息数量 + TTL time.Duration // 会话过期时间 + Compression bool // 是否启用压缩 + Compressor Compressor // 压缩器 + StorageConf StorageConfig // 存储配置 +} +``` + +## Redis 存储 + +```go +manager, err := context.NewManager(context.Config{ + Storage: "redis", + StorageConf: context.StorageConfig{ + Addr: "localhost:6379", + Password: "", + DB: 0, + }, + MaxSize: 100, +}) +``` + +## 上下文压缩 + +```go +// 启用自动压缩 +manager, _ := context.NewManager(context.Config{ + MaxSize: 100, + Compression: true, + Compressor: context.NewSummarizerCompressor(), +}) + +// 手动压缩 +ctx.Compress() +``` + +## AI 上下文使用 + +```go +// 获取格式化的上下文用于 AI 模型 +ctx := manager.GetSession("session-123") + +// 获取完整上下文 +prompt := ctx.FormatPrompt(context.FormatOptions{ + IncludeSystem: true, + MaxTokens: 2000, +}) + +// 或者获取消息列表 +messages := ctx.GetMessages() +// messages 可以直接传递给 OpenAI API 等 +``` + +## 中间件支持 + +```go +// 使用中间件扩展功能 +manager.Use(middleware.SessionLogger()) +manager.Use(middleware.MetricsCollector()) +manager.Use(middleware.RateLimiter(100)) +``` + +## 与 go-zero 集成 + +```go +// 在 handler 中使用 +type MyHandler struct { + ctxManager *context.Manager +} + +func (h *MyHandler) ChatHandler(w http.ResponseWriter, r *http.Request) { + var req types.ChatRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), 400) + return + } + + // 获取或创建会话 + session := h.ctxManager.GetOrCreateSession(req.SessionID) + + // 添加用户消息 + session.AddMessage(context.Message{ + Role: "user", + Content: req.Message, + }) + + // 调用 AI 模型 + aiContext := session.GetContextForAI() + response := callAI(aiContext) + + // 添加助手消息 + session.AddMessage(context.Message{ + Role: "assistant", + Content: response, + }) + + // 保存会话 + h.ctxManager.SaveSession(req.SessionID) + + // 返回响应 + json.NewEncoder(w).Encode(types.ChatResponse{ + Response: response, + }) +} +``` diff --git a/backend/api/profile.api b/backend/api/profile.api new file mode 100644 index 0000000..5ae6904 --- /dev/null +++ b/backend/api/profile.api @@ -0,0 +1,29 @@ +syntax = "v1" + +// ========== 类型定义 ========== +type ( + // 获取个人信息响应 + GetProfileResponse { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + Email string `json:"email"` // 邮箱 + Phone string `json:"phone"` // 手机号 + Avatar string `json:"avatar"` // 头像URL + Bio string `json:"bio"` // 个人简介 + Status int `json:"status"` // 状态 1-正常 2-禁用 + CreatedAt string `json:"createdAt"` // 创建时间 + UpdatedAt string `json:"updatedAt"` // 更新时间 + } + // 更新个人资料请求 + UpdateProfileRequest { + Username string `json:"username,optional" validate:"min=3,max=32"` // 用户名 + Phone string `json:"phone,optional"` // 手机号 + Avatar string `json:"avatar,optional"` // 头像URL + Bio string `json:"bio,optional"` // 个人简介 + } + // 修改密码请求 + ChangePasswordRequest { + OldPassword string `json:"oldPassword" validate:"required,min=6,max=32"` // 旧密码 + NewPassword string `json:"newPassword" validate:"required,min=6,max=32"` // 新密码 + } +) diff --git a/backend/api/user.api b/backend/api/user.api new file mode 100644 index 0000000..caf77fc --- /dev/null +++ b/backend/api/user.api @@ -0,0 +1,50 @@ +syntax = "v1" + +// ========== 类型定义 ========== +type ( + // 创建用户请求 + CreateUserRequest { + Username string `json:"username" validate:"required,min=3,max=32"` // 用户名 + Email string `json:"email" validate:"required,email"` // 邮箱 + Password string `json:"password" validate:"required,min=6,max=32"` // 密码 + Phone string `json:"phone,optional"` // 手机号 + } + // 用户信息 + UserInfo { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + Email string `json:"email"` // 邮箱 + Phone string `json:"phone"` // 手机号 + Status int `json:"status"` // 状态 1-正常 2-禁用 + CreatedAt string `json:"createdAt"` // 创建时间 + UpdatedAt string `json:"updatedAt"` // 更新时间 + } + // 更新用户请求 + UpdateUserRequest { + Id int64 `path:"id" validate:"required,min=1"` // 用户ID + Username string `json:"username,optional"` // 用户名 + Email string `json:"email,optional"` // 邮箱 + Phone string `json:"phone,optional"` // 手机号 + Status int `json:"status,optional"` // 状态 + } + // 用户列表查询请求 + UserListRequest { + Page int `form:"page,default=1"` // 页码 + PageSize int `form:"pageSize,default=10"` // 每页数量 + Keyword string `form:"keyword,optional"` // 关键词搜索 + Status int `form:"status,optional"` // 状态筛选 + } + // 用户列表响应 + UserListResponse { + Total int64 `json:"total"` // 总数 + List []UserInfo `json:"list"` // 用户列表 + } + // 删除用户请求 + DeleteUserRequest { + Id int64 `path:"id" validate:"required,min=1"` // 用户ID + } + // 获取用户详情请求 + GetUserRequest { + Id int64 `path:"id" validate:"required,min=1"` // 用户ID + } +) diff --git a/backend/base.api b/backend/base.api new file mode 100644 index 0000000..264ed2c --- /dev/null +++ b/backend/base.api @@ -0,0 +1,125 @@ +syntax = "v1" + +info ( + title: "Base 统一服务" + desc: "Base 项目统一 API 接口" + author: "author@example.com" + version: "v1.0" +) + +import "api/user.api" +import "api/profile.api" + +// ========== 通用响应类型 ========== +type ( + Response { + Code int `json:"code"` // 状态码 + Message string `json:"message"` // 消息 + Success bool `json:"success"` // 是否成功 + Data interface{} `json:"data"` // 数据 + } + // 登录响应 + LoginResponse { + Code int `json:"code"` // 状态码 + Message string `json:"message"` // 消息 + Success bool `json:"success"` // 是否成功 + Token string `json:"token"` // JWT Token + } + // 注册请求 + RegisterRequest { + Username string `json:"username" validate:"required,min=3,max=32"` // 用户名 + Email string `json:"email" validate:"required,email"` // 邮箱 + Password string `json:"password" validate:"required,min=6,max=32"` // 密码 + Phone string `json:"phone,optional"` // 手机号 + } + // 登录请求 + LoginRequest { + Email string `json:"email" validate:"required,email"` // 邮箱 + Password string `json:"password" validate:"required,min=6,max=32"` // 密码 + } + // 刷新Token请求 + RefreshTokenRequest { + Token string `json:"token" validate:"required"` // Token + } +) + +// ========== 服务定义 ========== +@server ( + prefix: /api/v1 + group: auth + middleware: Cors,Log +) +service base-api { + // ========== 认证接口 ========== + // 注册 + @doc "用户注册" + @handler register + post /register (RegisterRequest) returns (UserInfo) + + // 登录 + @doc "用户登录" + @handler login + post /login (LoginRequest) returns (LoginResponse) + + // 刷新Token + @doc "刷新Token" + @handler refreshToken + post /refresh (RefreshTokenRequest) returns (LoginResponse) +} + +@server ( + prefix: /api/v1 + group: user + middleware: Cors,Log,Auth +) +service base-api { + // ========== 用户管理接口 ========== + // 创建用户 + @doc "创建用户" + @handler createUser + post /user (CreateUserRequest) returns (UserInfo) + + // 获取用户列表 + @doc "获取用户列表" + @handler getUserList + get /users (UserListRequest) returns (UserListResponse) + + // 获取用户详情 + @doc "获取用户详情" + @handler getUser + get /user/:id (GetUserRequest) returns (UserInfo) + + // 更新用户 + @doc "更新用户信息" + @handler updateUser + put /user/:id (UpdateUserRequest) returns (UserInfo) + + // 删除用户 + @doc "删除用户" + @handler deleteUser + delete /user/:id (DeleteUserRequest) returns (Response) +} + +@server ( + prefix: /api/v1 + group: profile + middleware: Cors,Log,Auth +) +service base-api { + // ========== 个人中心接口 ========== + // 获取个人信息 + @doc "获取个人信息" + @handler getProfile + get /profile/me returns (GetProfileResponse) + + // 更新个人资料 + @doc "更新个人资料" + @handler updateProfile + put /profile/me (UpdateProfileRequest) returns (GetProfileResponse) + + // 修改密码 + @doc "修改密码" + @handler changePassword + post /profile/password (ChangePasswordRequest) returns (Response) +} + diff --git a/backend/base.go b/backend/base.go new file mode 100644 index 0000000..731a62c --- /dev/null +++ b/backend/base.go @@ -0,0 +1,34 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package main + +import ( + "flag" + "fmt" + + "github.com/youruser/base/internal/config" + "github.com/youruser/base/internal/handler" + "github.com/youruser/base/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/rest" +) + +var configFile = flag.String("f", "etc/base-api.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + + server := rest.MustNewServer(c.RestConf) + defer server.Stop() + + ctx := svc.NewServiceContext(c) + handler.RegisterHandlers(server, ctx) + + fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) + server.Start() +} diff --git a/backend/etc/base-api.yaml b/backend/etc/base-api.yaml new file mode 100644 index 0000000..bd41e71 --- /dev/null +++ b/backend/etc/base-api.yaml @@ -0,0 +1,7 @@ +Name: base-api +Host: 0.0.0.0 +Port: 8888 + +# MySQL 数据库配置 +MySQL: + DSN: root:dev123456@tcp(219.159.132.177:17173)/base?charset=utf8mb4&parseTime=true&loc=Local diff --git a/backend/go-zero.md b/backend/go-zero.md new file mode 100644 index 0000000..3653746 --- /dev/null +++ b/backend/go-zero.md @@ -0,0 +1,58 @@ +# go-zero 框架 + +> go-zero 是一个高性能的微服务框架,简化了服务的开发和部署。 + +## 官方文档 + +- [官方网站](https://go-zero.dev/) +- [GitHub](https://github.com/zeromicro/zero) +- [入门指南](https://go-zero.dev/docs/introduction) +- [快速开始](https://go-zero.dev/docs/getting-started) +- [API 开发](https://go-zero.dev/docs/task/api) +- [gRPC 开发](https://go-zero.dev/docs/task/grpc) + +## 核心特性 + +- 简单易用:内置代码生成工具,快速生成服务代码 +- 性能强大:基于 Go 语言,支持高并发 +- 完整工具链:提供 API 生成、gRPC 工具、微服务治理等 +- 云原生:支持 Kubernetes、Docker 等云原生部署 + +## 项目结构 + +``` +. +├── api # API 服务目录 +│ ├── etc # 配置文件 +│ ├── internal # 业务逻辑 +│ │ ├── handler # HTTP 处理器 +│ │ ├── logic # 业务逻辑 +│ │ ├── svc // 服务上下文 +│ │ └── types // 类型定义 +│ └── main.go # 入口文件 +└── rpc # gRPC 服务目录 + ├── etc # 配置文件 + ├── internal # 业务逻辑 + │ ├── logic // 业务逻辑 + │ └── svr // 服务器 + └── *.proto # Protobuf 定义 +``` + +## 安装 + +```bash +# 安装 goctl 工具 +go install github.com/zeromicro/zero-tools/goctl@latest + +# 创建 API 服务 +goctl api new api + +# 创建 gRPC 服务 +goctl rpc new rpc +``` + +## 相关 AI 支持 + +- ai-context: https://github.com/zeromicro/ai-context +- mcp-zero : https://github.com/zeromicro/mcp-zero +- zero-skills : https://github.com/zeromicro/zero-skills diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..e8a4022 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,59 @@ +module github.com/youruser/base + +go 1.25.0 + +require ( + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/zeromicro/go-zero v1.9.4 + gorm.io/driver/mysql v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grafana/pyroscope-go v1.2.7 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/prometheus/client_golang v1.21.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/sdk v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.33.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..fa273f0 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,146 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= +github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/zeromicro/go-zero v1.9.4 h1:aRLFoISqAYijABtkbliQC5SsI5TbizJpQvoHc9xup8k= +github.com/zeromicro/go-zero v1.9.4/go.mod h1:a17JOTch25SWxBcUgJZYps60hygK3pIYdw7nGwlcS38= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= +go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY= +go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= +google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..ea7cbb7 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,15 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package config + +import "github.com/zeromicro/go-zero/rest" + +type Config struct { + rest.RestConf + + // MySQL 数据库配置 + MySQL struct { + DSN string + } +} diff --git a/backend/internal/handler/auth/loginhandler.go b/backend/internal/handler/auth/loginhandler.go new file mode 100644 index 0000000..08bd551 --- /dev/null +++ b/backend/internal/handler/auth/loginhandler.go @@ -0,0 +1,32 @@ +// Code generated by goctl. Safe to edit. +// goctl 1.9.2 + +package auth + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/auth" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 用户登录 +func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.LoginRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := auth.NewLoginLogic(r.Context(), svcCtx) + resp, err := l.Login(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/auth/refreshtokenhandler.go b/backend/internal/handler/auth/refreshtokenhandler.go new file mode 100644 index 0000000..9696826 --- /dev/null +++ b/backend/internal/handler/auth/refreshtokenhandler.go @@ -0,0 +1,32 @@ +// Code generated by goctl. Safe to edit. +// goctl 1.9.2 + +package auth + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/auth" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 刷新Token +func RefreshTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RefreshTokenRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := auth.NewRefreshTokenLogic(r.Context(), svcCtx) + resp, err := l.RefreshToken(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/auth/registerhandler.go b/backend/internal/handler/auth/registerhandler.go new file mode 100644 index 0000000..3900bdf --- /dev/null +++ b/backend/internal/handler/auth/registerhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package auth + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/auth" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 用户注册 +func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RegisterRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := auth.NewRegisterLogic(r.Context(), svcCtx) + resp, err := l.Register(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/profile/changepasswordhandler.go b/backend/internal/handler/profile/changepasswordhandler.go new file mode 100644 index 0000000..bf616ef --- /dev/null +++ b/backend/internal/handler/profile/changepasswordhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package profile + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/profile" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 修改密码 +func ChangePasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ChangePasswordRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := profile.NewChangePasswordLogic(r.Context(), svcCtx) + resp, err := l.ChangePassword(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/profile/getprofilehandler.go b/backend/internal/handler/profile/getprofilehandler.go new file mode 100644 index 0000000..7fdb7ef --- /dev/null +++ b/backend/internal/handler/profile/getprofilehandler.go @@ -0,0 +1,25 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package profile + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/profile" + "github.com/youruser/base/internal/svc" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 获取个人信息 +func GetProfileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := profile.NewGetProfileLogic(r.Context(), svcCtx) + resp, err := l.GetProfile() + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/profile/updateprofilehandler.go b/backend/internal/handler/profile/updateprofilehandler.go new file mode 100644 index 0000000..2d5292b --- /dev/null +++ b/backend/internal/handler/profile/updateprofilehandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package profile + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/profile" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新个人资料 +func UpdateProfileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateProfileRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := profile.NewUpdateProfileLogic(r.Context(), svcCtx) + resp, err := l.UpdateProfile(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/routes.go b/backend/internal/handler/routes.go new file mode 100644 index 0000000..fbb8cd8 --- /dev/null +++ b/backend/internal/handler/routes.go @@ -0,0 +1,110 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.9.2 + +package handler + +import ( + "net/http" + + auth "github.com/youruser/base/internal/handler/auth" + profile "github.com/youruser/base/internal/handler/profile" + user "github.com/youruser/base/internal/handler/user" + "github.com/youruser/base/internal/svc" + + "github.com/zeromicro/go-zero/rest" +) + +func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.Cors, serverCtx.Log}, + []rest.Route{ + { + // 用户登录 + Method: http.MethodPost, + Path: "/login", + Handler: auth.LoginHandler(serverCtx), + }, + { + // 刷新Token + Method: http.MethodPost, + Path: "/refresh", + Handler: auth.RefreshTokenHandler(serverCtx), + }, + { + // 用户注册 + Method: http.MethodPost, + Path: "/register", + Handler: auth.RegisterHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.Cors, serverCtx.Log, serverCtx.Auth}, + []rest.Route{ + { + // 获取个人信息 + Method: http.MethodGet, + Path: "/profile/me", + Handler: profile.GetProfileHandler(serverCtx), + }, + { + // 更新个人资料 + Method: http.MethodPut, + Path: "/profile/me", + Handler: profile.UpdateProfileHandler(serverCtx), + }, + { + // 修改密码 + Method: http.MethodPost, + Path: "/profile/password", + Handler: profile.ChangePasswordHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.Cors, serverCtx.Log, serverCtx.Auth}, + []rest.Route{ + { + // 创建用户 + Method: http.MethodPost, + Path: "/user", + Handler: user.CreateUserHandler(serverCtx), + }, + { + // 获取用户详情 + Method: http.MethodGet, + Path: "/user/:id", + Handler: user.GetUserHandler(serverCtx), + }, + { + // 更新用户信息 + Method: http.MethodPut, + Path: "/user/:id", + Handler: user.UpdateUserHandler(serverCtx), + }, + { + // 删除用户 + Method: http.MethodDelete, + Path: "/user/:id", + Handler: user.DeleteUserHandler(serverCtx), + }, + { + // 获取用户列表 + Method: http.MethodGet, + Path: "/users", + Handler: user.GetUserListHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1"), + ) +} diff --git a/backend/internal/handler/user/createuserhandler.go b/backend/internal/handler/user/createuserhandler.go new file mode 100644 index 0000000..0e3d463 --- /dev/null +++ b/backend/internal/handler/user/createuserhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package user + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/user" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 创建用户 +func CreateUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateUserRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewCreateUserLogic(r.Context(), svcCtx) + resp, err := l.CreateUser(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/user/deleteuserhandler.go b/backend/internal/handler/user/deleteuserhandler.go new file mode 100644 index 0000000..2cfcc72 --- /dev/null +++ b/backend/internal/handler/user/deleteuserhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package user + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/user" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 删除用户 +func DeleteUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DeleteUserRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewDeleteUserLogic(r.Context(), svcCtx) + resp, err := l.DeleteUser(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/user/getuserhandler.go b/backend/internal/handler/user/getuserhandler.go new file mode 100644 index 0000000..ff24393 --- /dev/null +++ b/backend/internal/handler/user/getuserhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package user + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/user" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 获取用户详情 +func GetUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetUserRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewGetUserLogic(r.Context(), svcCtx) + resp, err := l.GetUser(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/user/getuserlisthandler.go b/backend/internal/handler/user/getuserlisthandler.go new file mode 100644 index 0000000..52afff4 --- /dev/null +++ b/backend/internal/handler/user/getuserlisthandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package user + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/user" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 获取用户列表 +func GetUserListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UserListRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewGetUserListLogic(r.Context(), svcCtx) + resp, err := l.GetUserList(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/handler/user/updateuserhandler.go b/backend/internal/handler/user/updateuserhandler.go new file mode 100644 index 0000000..24aa84b --- /dev/null +++ b/backend/internal/handler/user/updateuserhandler.go @@ -0,0 +1,32 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package user + +import ( + "net/http" + + "github.com/youruser/base/internal/logic/user" + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新用户信息 +func UpdateUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateUserRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := user.NewUpdateUserLogic(r.Context(), svcCtx) + resp, err := l.UpdateUser(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/backend/internal/logic/auth/loginlogic.go b/backend/internal/logic/auth/loginlogic.go new file mode 100644 index 0000000..eee513d --- /dev/null +++ b/backend/internal/logic/auth/loginlogic.go @@ -0,0 +1,69 @@ +package auth + +import ( + "context" + "crypto/md5" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/internal/util/jwt" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type LoginLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 用户登录 +func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic { + return &LoginLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) { + // 查询用户 + user, err := model.FindOneByEmail(l.ctx, l.svcCtx.DB, req.Email) + if err != nil { + if err == model.ErrNotFound { + return &types.LoginResponse{ + Code: 404, + Message: "用户不存在或密码错误", + Success: false, + }, nil + } + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + // 加密输入的密码并与数据库密码对比 + inputPassword := fmt.Sprintf("%x", md5.Sum([]byte(req.Password))) + if user.Password != inputPassword { + return &types.LoginResponse{ + Code: 400, + Message: "用户不存在或密码错误", + Success: false, + }, nil + } + + // 生成 Token + token, err := jwt.GenerateToken(user.Id, user.Username, user.Email) + if err != nil { + return nil, fmt.Errorf("生成Token失败: %v", err) + } + + l.Infof("登录成功,userId=%d", user.Id) + + return &types.LoginResponse{ + Code: 200, + Message: "登录成功", + Success: true, + Token: token, + }, nil +} diff --git a/backend/internal/logic/auth/refreshtokenlogic.go b/backend/internal/logic/auth/refreshtokenlogic.go new file mode 100644 index 0000000..4bd570d --- /dev/null +++ b/backend/internal/logic/auth/refreshtokenlogic.go @@ -0,0 +1,68 @@ +package auth + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/internal/util/jwt" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RefreshTokenLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 刷新 Token +func NewRefreshTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefreshTokenLogic { + return &RefreshTokenLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenRequest) (resp *types.LoginResponse, err error) { + // 解析并验证 Token + claims, err := jwt.ParseToken(req.Token) + if err != nil { + return &types.LoginResponse{ + Code: 401, + Message: "Token无效或已过期", + Success: false, + }, nil + } + + // 查询用户是否存在 + user, err := model.FindOne(l.ctx, l.svcCtx.DB, claims.UserID) + if err != nil { + if err == model.ErrNotFound { + return &types.LoginResponse{ + Code: 404, + Message: "用户不存在", + Success: false, + }, nil + } + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + // 生成新 Token + newToken, err := jwt.GenerateToken(user.Id, user.Username, user.Email) + if err != nil { + return nil, fmt.Errorf("生成Token失败: %v", err) + } + + l.Infof("刷新Token成功,userId=%d", user.Id) + + return &types.LoginResponse{ + Code: 200, + Message: "刷新Token成功", + Success: true, + Token: newToken, + }, nil +} diff --git a/backend/internal/logic/auth/registerlogic.go b/backend/internal/logic/auth/registerlogic.go new file mode 100644 index 0000000..b999fc0 --- /dev/null +++ b/backend/internal/logic/auth/registerlogic.go @@ -0,0 +1,77 @@ +package auth + +import ( + "context" + "crypto/md5" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RegisterLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 用户注册 +func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic { + return &RegisterLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RegisterLogic) Register(req *types.RegisterRequest) (resp *types.UserInfo, err error) { + // 检查邮箱是否已存在 + _, err = model.FindOneByEmail(l.ctx, l.svcCtx.DB, req.Email) + if err == nil { + return nil, fmt.Errorf("邮箱已被注册") + } + if err != model.ErrNotFound { + return nil, fmt.Errorf("检查邮箱失败: %v", err) + } + + // 创建用户模型 + user := &model.User{ + Username: req.Username, + Email: req.Email, + Password: fmt.Sprintf("%x", md5.Sum([]byte(req.Password))), // 密码加密 + Phone: req.Phone, + Status: 1, // 默认正常状态 + } + + // 插入数据库 + id, err := model.Insert(l.ctx, l.svcCtx.DB, user) + if err != nil { + return nil, fmt.Errorf("创建用户失败: %v", err) + } + + // 查询创建的用户 + user, err = model.FindOne(l.ctx, l.svcCtx.DB, id) + if err != nil { + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + // 返回用户信息(不返回密码) + resp = &types.UserInfo{ + Id: user.Id, + Username: user.Username, + Email: user.Email, + Phone: user.Phone, + Status: int(user.Status), + CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + // 返回 Token 在响应头中(通过中间件处理) + // 临时方案:将 token 放入响应 Data 中 + l.Infof("注册成功,userId=%d", user.Id) + + return resp, nil +} diff --git a/backend/internal/logic/profile/changepasswordlogic.go b/backend/internal/logic/profile/changepasswordlogic.go new file mode 100644 index 0000000..cfb3c4f --- /dev/null +++ b/backend/internal/logic/profile/changepasswordlogic.go @@ -0,0 +1,73 @@ +package profile + +import ( + "context" + "crypto/md5" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ChangePasswordLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 修改密码 +func NewChangePasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ChangePasswordLogic { + return &ChangePasswordLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ChangePasswordLogic) ChangePassword(req *types.ChangePasswordRequest) (resp *types.Response, err error) { + // 从上下文中获取当前用户ID + userIdValue := l.ctx.Value("userId") + if userIdValue == nil { + return nil, fmt.Errorf("未获取到用户信息,请先登录") + } + userId, ok := userIdValue.(int64) + if !ok { + return nil, fmt.Errorf("用户ID格式错误") + } + + // 查询用户 + user, err := model.FindOne(l.ctx, l.svcCtx.DB, userId) + if err != nil { + if err == model.ErrNotFound { + return nil, fmt.Errorf("用户不存在") + } + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + // 加密旧密码 + oldEncrypted := fmt.Sprintf("%x", md5.Sum([]byte(req.OldPassword))) + if user.Password != oldEncrypted { + return &types.Response{ + Code: 400, + Message: "旧密码错误", + }, nil + } + + // 加密新密码 + newEncrypted := fmt.Sprintf("%x", md5.Sum([]byte(req.NewPassword))) + user.Password = newEncrypted + + // 更新数据库 + err = model.Update(l.ctx, l.svcCtx.DB, user) + if err != nil { + return nil, fmt.Errorf("修改密码失败: %v", err) + } + + return &types.Response{ + Code: 200, + Message: "修改密码成功", + }, nil +} diff --git a/backend/internal/logic/profile/getprofilelogic.go b/backend/internal/logic/profile/getprofilelogic.go new file mode 100644 index 0000000..ddc15b8 --- /dev/null +++ b/backend/internal/logic/profile/getprofilelogic.go @@ -0,0 +1,76 @@ +package profile + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetProfileLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 获取个人信息 +func NewGetProfileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetProfileLogic { + return &GetProfileLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetProfileLogic) GetProfile() (resp *types.GetProfileResponse, err error) { + // 从上下文中获取当前用户ID(假设通过 Auth 中间件设置) + userIdValue := l.ctx.Value("userId") + if userIdValue == nil { + return nil, fmt.Errorf("未获取到用户信息,请先登录") + } + userId, ok := userIdValue.(int64) + if !ok { + return nil, fmt.Errorf("用户ID格式错误") + } + + // 查询用户信息 + user, err := model.FindOne(l.ctx, l.svcCtx.DB, userId) + if err != nil { + if err == model.ErrNotFound { + return nil, fmt.Errorf("用户不存在") + } + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + // 查询个人资料 + profile, err := model.FindProfileByUserId(l.ctx, l.svcCtx.DB, userId) + if err != nil && err != model.ErrNotFound { + return nil, fmt.Errorf("查询个人资料失败: %v", err) + } + + // 如果没有个人资料,使用默认值 + avatar := "" + bio := "" + if profile != nil { + avatar = profile.Avatar + bio = profile.Bio + } + + resp = &types.GetProfileResponse{ + Id: user.Id, + Username: user.Username, + Email: user.Email, + Phone: user.Phone, + Avatar: avatar, + Bio: bio, + Status: int(user.Status), + CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + return resp, nil +} diff --git a/backend/internal/logic/profile/updateprofilelogic.go b/backend/internal/logic/profile/updateprofilelogic.go new file mode 100644 index 0000000..2f26bc5 --- /dev/null +++ b/backend/internal/logic/profile/updateprofilelogic.go @@ -0,0 +1,121 @@ +package profile + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateProfileLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新个人资料 +func NewUpdateProfileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateProfileLogic { + return &UpdateProfileLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateProfileLogic) UpdateProfile(req *types.UpdateProfileRequest) (resp *types.GetProfileResponse, err error) { + // 从上下文中获取当前用户ID + userIdValue := l.ctx.Value("userId") + if userIdValue == nil { + return nil, fmt.Errorf("未获取到用户信息,请先登录") + } + userId, ok := userIdValue.(int64) + if !ok { + return nil, fmt.Errorf("用户ID格式错误") + } + + // 查询用户 + user, err := model.FindOne(l.ctx, l.svcCtx.DB, userId) + if err != nil { + if err == model.ErrNotFound { + return nil, fmt.Errorf("用户不存在") + } + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + // 更新用户基本信息 + if req.Username != "" { + user.Username = req.Username + } + if req.Phone != "" { + user.Phone = req.Phone + } + + // 更新用户表 + err = model.Update(l.ctx, l.svcCtx.DB, user) + if err != nil { + return nil, fmt.Errorf("更新用户信息失败: %v", err) + } + + // 查询或创建个人资料 + profile, err := model.FindProfileByUserId(l.ctx, l.svcCtx.DB, userId) + if err == model.ErrNotFound { + // 创建新的个人资料 + profile = &model.Profile{ + UserId: userId, + Avatar: req.Avatar, + Bio: req.Bio, + } + _, err = model.InsertProfile(l.ctx, l.svcCtx.DB, profile) + if err != nil { + return nil, fmt.Errorf("创建个人资料失败: %v", err) + } + } else if err != nil { + return nil, fmt.Errorf("查询个人资料失败: %v", err) + } else { + // 更新现有个人资料 + if req.Avatar != "" { + profile.Avatar = req.Avatar + } + if req.Bio != "" { + profile.Bio = req.Bio + } + err = model.UpdateProfile(l.ctx, l.svcCtx.DB, profile) + if err != nil { + return nil, fmt.Errorf("更新个人资料失败: %v", err) + } + } + + // 重新查询用户信息 + user, err = model.FindOne(l.ctx, l.svcCtx.DB, userId) + if err != nil { + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + // 查询个人资料 + profile, _ = model.FindProfileByUserId(l.ctx, l.svcCtx.DB, userId) + + avatar := "" + bio := "" + if profile != nil { + avatar = profile.Avatar + bio = profile.Bio + } + + resp = &types.GetProfileResponse{ + Id: user.Id, + Username: user.Username, + Email: user.Email, + Phone: user.Phone, + Avatar: avatar, + Bio: bio, + Status: int(user.Status), + CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + return resp, nil +} diff --git a/backend/internal/logic/user/createuserlogic.go b/backend/internal/logic/user/createuserlogic.go new file mode 100644 index 0000000..00cbd91 --- /dev/null +++ b/backend/internal/logic/user/createuserlogic.go @@ -0,0 +1,76 @@ +package user + +import ( + "context" + "crypto/md5" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateUserLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 创建用户 +func NewCreateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserLogic { + return &CreateUserLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) (resp *types.UserInfo, err error) { + // 检查邮箱是否已存在 + _, err = model.FindOneByEmail(l.ctx, l.svcCtx.DB, req.Email) + if err == nil { + return nil, fmt.Errorf("邮箱已被注册") + } + if err != model.ErrNotFound { + return nil, fmt.Errorf("检查邮箱失败: %v", err) + } + + // 密码加密(简单MD5,实际项目应使用bcrypt等更安全的加密) + encryptedPassword := fmt.Sprintf("%x", md5.Sum([]byte(req.Password))) + + // 创建用户模型 + user := &model.User{ + Username: req.Username, + Email: req.Email, + Password: encryptedPassword, + Phone: req.Phone, + Status: 1, // 默认正常状态 + } + + // 插入数据库 + id, err := model.Insert(l.ctx, l.svcCtx.DB, user) + if err != nil { + return nil, fmt.Errorf("创建用户失败: %v", err) + } + + // 查询创建的用户 + user, err = model.FindOne(l.ctx, l.svcCtx.DB, id) + if err != nil { + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + // 返回用户信息(不返回密码) + resp = &types.UserInfo{ + Id: user.Id, + Username: user.Username, + Email: user.Email, + Phone: user.Phone, + Status: int(user.Status), + CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + return resp, nil +} diff --git a/backend/internal/logic/user/deleteuserlogic.go b/backend/internal/logic/user/deleteuserlogic.go new file mode 100644 index 0000000..79bf1dc --- /dev/null +++ b/backend/internal/logic/user/deleteuserlogic.go @@ -0,0 +1,56 @@ +package user + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteUserLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 删除用户 +func NewDeleteUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteUserLogic { + return &DeleteUserLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteUserLogic) DeleteUser(req *types.DeleteUserRequest) (resp *types.Response, err error) { + // 检查用户是否存在 + user, err := model.FindOne(l.ctx, l.svcCtx.DB, req.Id) + if err != nil { + if err == model.ErrNotFound { + return &types.Response{ + Code: 404, + Message: "用户不存在", + Success: false, + }, nil + } + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + // 删除用户 + err = model.Delete(l.ctx, l.svcCtx.DB, req.Id) + if err != nil { + return nil, fmt.Errorf("删除用户失败: %v", err) + } + + l.Infof("删除用户成功: id=%d, username=%s", user.Id, user.Username) + + return &types.Response{ + Code: 200, + Message: "删除成功", + Success: true, + }, nil +} diff --git a/backend/internal/logic/user/getuserlistlogic.go b/backend/internal/logic/user/getuserlistlogic.go new file mode 100644 index 0000000..b06dd7f --- /dev/null +++ b/backend/internal/logic/user/getuserlistlogic.go @@ -0,0 +1,57 @@ +package user + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUserListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 获取用户列表 +func NewGetUserListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserListLogic { + return &GetUserListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserListLogic) GetUserList(req *types.UserListRequest) (resp *types.UserListResponse, err error) { + // 查询数据库 + users, total, err := model.FindList(l.ctx, l.svcCtx.DB, + int64(req.Page), int64(req.PageSize), req.Keyword, int64(req.Status)) + if err != nil { + return nil, fmt.Errorf("查询用户列表失败: %v", err) + } + + // 转换为返回类型 + list := make([]types.UserInfo, 0, len(users)) + for _, user := range users { + list = append(list, types.UserInfo{ + Id: user.Id, + Username: user.Username, + Email: user.Email, + Phone: user.Phone, + Status: int(user.Status), + CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), + }) + } + + resp = &types.UserListResponse{ + Total: total, + List: list, + } + + return resp, nil +} diff --git a/backend/internal/logic/user/getuserlogic.go b/backend/internal/logic/user/getuserlogic.go new file mode 100644 index 0000000..16b318b --- /dev/null +++ b/backend/internal/logic/user/getuserlogic.go @@ -0,0 +1,51 @@ +package user + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUserLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 获取用户详情 +func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic { + return &GetUserLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (resp *types.UserInfo, err error) { + // 根据ID查询用户 + user, err := model.FindOne(l.ctx, l.svcCtx.DB, req.Id) + if err != nil { + if err == model.ErrNotFound { + return nil, fmt.Errorf("用户不存在") + } + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + // 返回用户信息 + resp = &types.UserInfo{ + Id: user.Id, + Username: user.Username, + Email: user.Email, + Phone: user.Phone, + Status: int(user.Status), + CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + return resp, nil +} diff --git a/backend/internal/logic/user/updateuserlogic.go b/backend/internal/logic/user/updateuserlogic.go new file mode 100644 index 0000000..34ee76b --- /dev/null +++ b/backend/internal/logic/user/updateuserlogic.go @@ -0,0 +1,81 @@ +package user + +import ( + "context" + "fmt" + + "github.com/youruser/base/internal/svc" + "github.com/youruser/base/internal/types" + "github.com/youruser/base/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateUserLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新用户信息 +func NewUpdateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserLogic { + return &UpdateUserLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserLogic) UpdateUser(req *types.UpdateUserRequest) (resp *types.UserInfo, err error) { + // 检查用户是否存在 + user, err := model.FindOne(l.ctx, l.svcCtx.DB, req.Id) + if err != nil { + if err == model.ErrNotFound { + return nil, fmt.Errorf("用户不存在") + } + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + // 更新字段 + if req.Username != "" { + user.Username = req.Username + } + if req.Email != "" { + // 检查邮箱是否被其他用户使用 + existing, err := model.FindOneByEmail(l.ctx, l.svcCtx.DB, req.Email) + if err == nil && existing.Id != req.Id { + return nil, fmt.Errorf("邮箱已被使用") + } + user.Email = req.Email + } + if req.Phone != "" { + user.Phone = req.Phone + } + if req.Status > 0 { + user.Status = int64(req.Status) + } + + // 更新数据库 + err = model.Update(l.ctx, l.svcCtx.DB, user) + if err != nil { + return nil, fmt.Errorf("更新用户失败: %v", err) + } + + // 重新查询获取更新后的数据 + user, err = model.FindOne(l.ctx, l.svcCtx.DB, req.Id) + if err != nil { + return nil, fmt.Errorf("查询用户失败: %v", err) + } + + resp = &types.UserInfo{ + Id: user.Id, + Username: user.Username, + Email: user.Email, + Phone: user.Phone, + Status: int(user.Status), + CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + return resp, nil +} diff --git a/backend/internal/middleware/authmiddleware.go b/backend/internal/middleware/authmiddleware.go new file mode 100644 index 0000000..e3004f1 --- /dev/null +++ b/backend/internal/middleware/authmiddleware.go @@ -0,0 +1,48 @@ +package middleware + +import ( + "context" + "net/http" + "strings" + + "github.com/youruser/base/internal/util/jwt" +) + +type AuthMiddleware struct{} + +func NewAuthMiddleware() *AuthMiddleware { + return &AuthMiddleware{} +} + +func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 从 Header 中获取 Token + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Token 格式: "Bearer " + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if tokenString == "" { + http.Error(w, "Invalid token format", http.StatusUnauthorized) + return + } + + // 解析并验证 Token + claims, err := jwt.ParseToken(tokenString) + if err != nil { + http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized) + return + } + + // 将 userId 存入上下文,供后续 logic 使用 + ctx := context.WithValue(r.Context(), "userId", claims.UserID) + ctx = context.WithValue(ctx, "username", claims.Username) + ctx = context.WithValue(ctx, "email", claims.Email) + + // 传递给下一个处理器 + next(w, r.WithContext(ctx)) + } +} diff --git a/backend/internal/middleware/corsmiddleware.go b/backend/internal/middleware/corsmiddleware.go new file mode 100644 index 0000000..e8e61c5 --- /dev/null +++ b/backend/internal/middleware/corsmiddleware.go @@ -0,0 +1,22 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package middleware + +import "net/http" + +type CorsMiddleware struct { +} + +func NewCorsMiddleware() *CorsMiddleware { + return &CorsMiddleware{} +} + +func (m *CorsMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // TODO generate middleware implement function, delete after code implementation + + // Passthrough to next handler if need + next(w, r) + } +} diff --git a/backend/internal/middleware/logmiddleware.go b/backend/internal/middleware/logmiddleware.go new file mode 100644 index 0000000..4136d01 --- /dev/null +++ b/backend/internal/middleware/logmiddleware.go @@ -0,0 +1,22 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package middleware + +import "net/http" + +type LogMiddleware struct { +} + +func NewLogMiddleware() *LogMiddleware { + return &LogMiddleware{} +} + +func (m *LogMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // TODO generate middleware implement function, delete after code implementation + + // Passthrough to next handler if need + next(w, r) + } +} diff --git a/backend/internal/svc/servicecontext.go b/backend/internal/svc/servicecontext.go new file mode 100644 index 0000000..3e7ab36 --- /dev/null +++ b/backend/internal/svc/servicecontext.go @@ -0,0 +1,59 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.9.2 + +package svc + +import ( + "github.com/youruser/base/internal/config" + "github.com/youruser/base/internal/middleware" + "github.com/youruser/base/model" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + + "github.com/zeromicro/go-zero/rest" +) + +type ServiceContext struct { + Config config.Config + Cors rest.Middleware + Log rest.Middleware + Auth rest.Middleware + // 数据库连接 + DB *gorm.DB +} + +func NewServiceContext(c config.Config) *ServiceContext { + // 创建数据库连接 + dsn := c.MySQL.DSN + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + panic("Failed to connect database: " + err.Error()) + } + + // 自动迁移表 + err = db.AutoMigrate(&model.User{}, &model.Profile{}) + if err != nil { + panic("Failed to migrate database: " + err.Error()) + } + + return &ServiceContext{ + Config: c, + Cors: middleware.NewCorsMiddleware().Handle, + Log: middleware.NewLogMiddleware().Handle, + Auth: middleware.NewAuthMiddleware().Handle, + DB: db, + } +} + +// Close 关闭资源 +func (s *ServiceContext) Close() error { + if s.DB != nil { + sqlDB, err := s.DB.DB() + if err != nil { + return err + } + return sqlDB.Close() + } + return nil +} diff --git a/backend/internal/types/types.go b/backend/internal/types/types.go new file mode 100644 index 0000000..31f884d --- /dev/null +++ b/backend/internal/types/types.go @@ -0,0 +1,103 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.9.2 + +package types + +type ChangePasswordRequest struct { + OldPassword string `json:"oldPassword" validate:"required,min=6,max=32"` // 旧密码 + NewPassword string `json:"newPassword" validate:"required,min=6,max=32"` // 新密码 +} + +type CreateUserRequest struct { + Username string `json:"username" validate:"required,min=3,max=32"` // 用户名 + Email string `json:"email" validate:"required,email"` // 邮箱 + Password string `json:"password" validate:"required,min=6,max=32"` // 密码 + Phone string `json:"phone,optional"` // 手机号 +} + +type DeleteUserRequest struct { + Id int64 `path:"id" validate:"required,min=1"` // 用户ID +} + +type GetProfileResponse struct { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + Email string `json:"email"` // 邮箱 + Phone string `json:"phone"` // 手机号 + Avatar string `json:"avatar"` // 头像URL + Bio string `json:"bio"` // 个人简介 + Status int `json:"status"` // 状态 1-正常 2-禁用 + CreatedAt string `json:"createdAt"` // 创建时间 + UpdatedAt string `json:"updatedAt"` // 更新时间 +} + +type GetUserRequest struct { + Id int64 `path:"id" validate:"required,min=1"` // 用户ID +} + +type LoginRequest struct { + Email string `json:"email" validate:"required,email"` // 邮箱 + Password string `json:"password" validate:"required,min=6,max=32"` // 密码 +} + +type LoginResponse struct { + Code int `json:"code"` // 状态码 + Message string `json:"message"` // 消息 + Success bool `json:"success"` // 是否成功 + Token string `json:"token"` // JWT Token +} + +type RefreshTokenRequest struct { + Token string `json:"token" validate:"required"` // Token +} + +type RegisterRequest struct { + Username string `json:"username" validate:"required,min=3,max=32"` // 用户名 + Email string `json:"email" validate:"required,email"` // 邮箱 + Password string `json:"password" validate:"required,min=6,max=32"` // 密码 + Phone string `json:"phone,optional"` // 手机号 +} + +type Response struct { + Code int `json:"code"` // 状态码 + Message string `json:"message"` // 消息 + Success bool `json:"success"` // 是否成功 + Data interface{} `json:"data"` // 数据 +} + +type UpdateProfileRequest struct { + Username string `json:"username,optional" validate:"min=3,max=32"` // 用户名 + Phone string `json:"phone,optional"` // 手机号 + Avatar string `json:"avatar,optional"` // 头像URL + Bio string `json:"bio,optional"` // 个人简介 +} + +type UpdateUserRequest struct { + Id int64 `path:"id" validate:"required,min=1"` // 用户ID + Username string `json:"username,optional"` // 用户名 + Email string `json:"email,optional"` // 邮箱 + Phone string `json:"phone,optional"` // 手机号 + Status int `json:"status,optional"` // 状态 +} + +type UserInfo struct { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + Email string `json:"email"` // 邮箱 + Phone string `json:"phone"` // 手机号 + Status int `json:"status"` // 状态 1-正常 2-禁用 + CreatedAt string `json:"createdAt"` // 创建时间 + UpdatedAt string `json:"updatedAt"` // 更新时间 +} + +type UserListRequest struct { + Page int `form:"page,default=1"` // 页码 + PageSize int `form:"pageSize,default=10"` // 每页数量 + Keyword string `form:"keyword,optional"` // 关键词搜索 + Status int `form:"status,optional"` // 状态筛选 +} + +type UserListResponse struct { + Total int64 `json:"total"` // 总数 + List []UserInfo `json:"list"` // 用户列表 +} diff --git a/backend/internal/util/jwt/jwt.go b/backend/internal/util/jwt/jwt.go new file mode 100644 index 0000000..24ed3ec --- /dev/null +++ b/backend/internal/util/jwt/jwt.go @@ -0,0 +1,59 @@ +package jwt + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var ( + // Token 有效期(7天) + TokenExpireTime = time.Hour * 24 * 7 + + // 签名 + SigningKey = []byte("base-jwt-secret-key-2024") +) + +type Claims struct { + UserID int64 `json:"userId"` + Username string `json:"username"` + Email string `json:"email"` + jwt.RegisteredClaims +} + +// GenerateToken 生成 JWT Token +func GenerateToken(userId int64, username, email string) (string, error) { + claims := Claims{ + UserID: userId, + Username: username, + Email: email, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenExpireTime)), + Issuer: "base-api", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(SigningKey) +} + +// ParseToken 解析并验证 JWT Token +func ParseToken(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("invalid signing method") + } + return SigningKey, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("invalid token") +} diff --git a/backend/mcp-zero.md b/backend/mcp-zero.md new file mode 100644 index 0000000..7814e93 --- /dev/null +++ b/backend/mcp-zero.md @@ -0,0 +1,60 @@ +# MCP Zero - Model Context Protocol for Go + +> mcp-zero 是 go-zero 的 Model Context Protocol (MCP) 实现,用于 AI 上下文管理和集成。 + +## 官方资源 + +- [GitHub](https://github.com/zeromicro/mcp-zero) +- [MCP 规范](https://modelcontextprotocol.io/) + +## 什么是 MCP + +Model Context Protocol (MCP) 是一种开放协议,用于在 AI 模型和外部工具/数据源之间建立标准化的通信方式。 + +## 功能特性 + +- 标准化的上下文传输协议 +- 支持多种数据源的连接 +- 可扩展的工具调用机制 +- 类型安全的 Go 实现 + +## 安装 + +```bash +go get github.com/zeromicro/mcp-zero +``` + +## 基本使用 + +```go +import "github.com/zeromicro/mcp-zero" + +// 初始化 MCP 客户端 +client := mcp.NewClient(mcp.Config{ + Host: "localhost", + Port: 8080, +}) + +// 注册工具 +client.RegisterTool("my_tool", func(ctx context.Context, args mcp.ToolArgs) (interface{}, error) { + // 工具逻辑 + return nil, nil +}) +``` + +## 配置示例 + +```yaml +mcp: + enabled: true + server: + host: "0.0.0.0" + port: 8080 + tools: + - name: "database_query" + description: "Query database" + enabled: true + - name: "api_call" + description: "Make API calls" + enabled: true +``` diff --git a/backend/model/profile_entity.go b/backend/model/profile_entity.go new file mode 100644 index 0000000..35f9acb --- /dev/null +++ b/backend/model/profile_entity.go @@ -0,0 +1,20 @@ +package model + +import ( + "time" +) + +// Profile 用户个人资料模型 +type Profile struct { + Id int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + UserId int64 `gorm:"column:user_id;not null;uniqueIndex" json:"userId"` // 关联用户ID + Avatar string `gorm:"column:avatar;type:varchar(255);default:''" json:"avatar"` // 头像URL + Bio string `gorm:"column:bio;type:text" json:"bio"` // 个人简介 + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"` + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"` +} + +// TableName 指定表名 +func (Profile) TableName() string { + return "profile" +} diff --git a/backend/model/profile_model.go b/backend/model/profile_model.go new file mode 100644 index 0000000..d8fe5d1 --- /dev/null +++ b/backend/model/profile_model.go @@ -0,0 +1,52 @@ +package model + +import ( + "context" + "errors" + "gorm.io/gorm" +) + +// InsertProfile 插入个人资料 +func InsertProfile(ctx context.Context, db *gorm.DB, profile *Profile) (int64, error) { + result := db.WithContext(ctx).Create(profile) + if result.Error != nil { + return 0, result.Error + } + return profile.Id, nil +} + +// FindOneProfile 根据ID查询个人资料 +func FindOneProfile(ctx context.Context, db *gorm.DB, id int64) (*Profile, error) { + var profile Profile + result := db.WithContext(ctx).First(&profile, id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, result.Error + } + return &profile, nil +} + +// FindProfileByUserId 根据用户ID查询个人资料 +func FindProfileByUserId(ctx context.Context, db *gorm.DB, userId int64) (*Profile, error) { + var profile Profile + result := db.WithContext(ctx).Where("user_id = ?", userId).First(&profile) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, result.Error + } + return &profile, nil +} + +// UpdateProfile 更新个人资料 +func UpdateProfile(ctx context.Context, db *gorm.DB, profile *Profile) error { + return db.WithContext(ctx).Save(profile).Error +} + +// DeleteProfile 删除个人资料 +func DeleteProfile(ctx context.Context, db *gorm.DB, id int64) error { + return db.WithContext(ctx).Delete(&Profile{}, id).Error +} diff --git a/backend/model/user_entity.go b/backend/model/user_entity.go new file mode 100644 index 0000000..cfe7a3c --- /dev/null +++ b/backend/model/user_entity.go @@ -0,0 +1,22 @@ +package model + +import ( + "time" +) + +// User 用户模型 +type User struct { + Id int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` + Username string `gorm:"column:username;type:varchar(32);not null" json:"username"` + Email string `gorm:"column:email;type:varchar(128);not null" json:"email"` + Password string `gorm:"column:password;type:varbinary(64);not null" json:"-"` + Phone string `gorm:"column:phone;type:varchar(20);default:''" json:"phone"` + Status int64 `gorm:"column:status;type:tinyint;default:1" json:"status"` + CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"` + UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"` +} + +// TableName 指定表名 +func (User) TableName() string { + return "user" +} diff --git a/backend/model/user_model.go b/backend/model/user_model.go new file mode 100644 index 0000000..78ce3e4 --- /dev/null +++ b/backend/model/user_model.go @@ -0,0 +1,94 @@ +package model + +import ( + "context" + "errors" + "gorm.io/gorm" +) + +var ( + ErrNotFound = errors.New("record not found") +) + +// Insert 插入用户 +func Insert(ctx context.Context, db *gorm.DB, user *User) (int64, error) { + result := db.WithContext(ctx).Create(user) + if result.Error != nil { + return 0, result.Error + } + return user.Id, nil +} + +// FindOne 根据ID查询用户 +func FindOne(ctx context.Context, db *gorm.DB, id int64) (*User, error) { + var user User + result := db.WithContext(ctx).First(&user, id) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, result.Error + } + return &user, nil +} + +// FindOneByEmail 根据邮箱查询用户 +func FindOneByEmail(ctx context.Context, db *gorm.DB, email string) (*User, error) { + var user User + result := db.WithContext(ctx).Where("email = ?", email).First(&user) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, ErrNotFound + } + return nil, result.Error + } + return &user, nil +} + +// Update 更新用户 +func Update(ctx context.Context, db *gorm.DB, user *User) error { + result := db.WithContext(ctx).Save(user) + return result.Error +} + +// Delete 删除用户 +func Delete(ctx context.Context, db *gorm.DB, id int64) error { + result := db.WithContext(ctx).Delete(&User{}, id) + return result.Error +} + +// FindList 查询用户列表 +func FindList(ctx context.Context, db *gorm.DB, page, pageSize int64, keyword string, status int64) ([]User, int64, error) { + var users []User + var total int64 + + query := db.WithContext(ctx).Model(&User{}) + + // 关键字搜索 + if keyword != "" { + query = query.Where("username LIKE ? OR email LIKE ? OR phone LIKE ?", + "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") + } + + // 状态筛选 + if status > 0 { + query = query.Where("status = ?", status) + } + + // 统计总数 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 分页查询 + offset := (page - 1) * pageSize + if offset < 0 { + offset = 0 + } + err := query.Offset(int(offset)).Limit(int(pageSize)).Find(&users).Error + if err != nil { + return nil, 0, err + } + + return users, total, nil +} diff --git a/backend/tests/USER_MODULE_TEST_STANDARD.md b/backend/tests/USER_MODULE_TEST_STANDARD.md new file mode 100644 index 0000000..a15d527 --- /dev/null +++ b/backend/tests/USER_MODULE_TEST_STANDARD.md @@ -0,0 +1,333 @@ +# User 模块测试标准流程 + +## 基础测试流程 + +每个模块应遵循以下基础测试流程: + +``` +新增(创建) -> 查询(单条) -> 更新 -> 查询(验证更新) -> 列表查询 -> 删除 -> 查询(验证删除) +``` + +## 测试步骤详解 + +### 1. 新增(创建) +创建新记录并返回 ID + +**验证点:** +- 记录成功插入数据库 +- 返回正确的 ID +- 必填字段验证通过 +- 重复数据检查(如邮箱唯一) + +**示例请求:** +```bash +curl -X POST http://localhost:8888/api/v1/user \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser01", + "email": "test01@example.com", + "password": "password123", + "phone": "13800138000" + }' +``` + +--- + +### 2. 查询(单条) +使用步骤 1 返回的 ID 查询记录 + +**验证点:** +- 返回完整记录信息 +- 字段值正确 +- 状态码为 200 + +**示例请求:** +```bash +curl -X GET http://localhost:8888/api/v1/user/1 +``` + +--- + +### 3. 更新 +使用 ID 更新记录的部分字段 + +**验证点:** +- 字段值正确更新 +- 返回更新后的完整记录 +- 其他字段保持不变 +- 数据验证(如邮箱唯一性) + +**示例请求:** +```bash +curl -X PUT http://localhost:8888/api/v1/user/1 \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser01_updated", + "phone": "13900139000" + }' +``` + +--- + +### 4. 查询(验证更新) +再次使用 ID 查询记录,验证更新结果 + +**验证点:** +- 更新的字段值正确 +- 未更新的字段值保持不变 + +**示例请求:** +```bash +curl -X GET http://localhost:8888/api/v1/user/1 +``` + +--- + +### 5. 列表查询 +查询记录列表,支持分页和筛选 + +**验证点:** +- 返回列表数据 +- 分页参数生效 +- 筛选条件生效 +- 总数统计正确 + +**示例请求:** +```bash +# 查询所有 +curl -X GET "http://localhost:8888/api/v1/users?page=1&pageSize=10" + +# 带关键词搜索 +curl -X GET "http://localhost:8888/api/v1/users?keyword=test&status=1" +``` + +--- + +### 6. 删除 +使用 ID 删除记录 + +**验证点:** +- 删除成功 +- 返回正确的响应格式 + +**示例请求:** +```bash +curl -X DELETE http://localhost:8888/api/v1/user/1 +``` + +--- + +### 7. 查询(验证删除) +再次使用 ID 查询记录,验证已删除 + +**验证点:** +- 返回 404 或相应错误信息 +- 记录确实已不存在 + +**示例请求:** +```bash +curl -X GET http://localhost:8888/api/v1/user/1 +``` + +--- + +## 测试脚本模板 + +### Bash 脚本模板 + +```bash +#!/bin/bash + +# 配置 +BASE_URL="http://localhost:8888/api/v1" +MODULE="user" + +# 日志函数 +log_info() { + echo -e "\033[32m[INFO]\033[0m $1" +} + +log_success() { + echo -e "\033[32m[SUCCESS]\033[0m $1" +} + +log_error() { + echo -e "\033[31m[ERROR]\033[0m $1" +} + +# 1. 新增 +log_info "步骤 1: 新增 $MODULE" +CREATE_RESULT=$(curl -s -X POST ${BASE_URL}/${MODULE} \ + -H "Content-Type: application/json" \ + -d '{ + "username": "test_'$(date +%s)'", + "email": "test_'$(date +%s)'@example.com", + "password": "password123", + "phone": "13800138000" + }') + +echo $CREATE_RESULT | jq '.' +ID=$(echo $CREATE_RESULT | jq -r '.id') + +# 2. 查询 +log_info "步骤 2: 查询 $MODULE (ID: $ID)" +QUERY_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/${ID}) +echo $QUERY_RESULT | jq '.' + +# 3. 更新 +log_info "步骤 3: 更新 $MODULE" +UPDATE_RESULT=$(curl -s -X PUT ${BASE_URL}/${MODULE}/${ID} \ + -H "Content-Type: application/json" \ + -d '{ + "phone": "13900139000" + }') +echo $UPDATE_RESULT | jq '.' + +# 4. 验证更新 +log_info "步骤 4: 验证更新" +VERIFY_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/${ID}) +echo $VERIFY_RESULT | jq '.' + +# 5. 列表查询 +log_info "步骤 5: 列表查询" +LIST_RESULT=$(curl -s -X GET "${BASE_URL}/${MODULE}s?page=1&pageSize=10") +echo $LIST_RESULT | jq '.' + +# 6. 删除 +log_info "步骤 6: 删除 $MODULE" +DELETE_RESULT=$(curl -s -X DELETE ${BASE_URL}/${MODULE}/${ID}) +echo $DELETE_RESULT | jq '.' + +# 7. 验证删除 +log_info "步骤 7: 验证删除" +VERIFY_DELETE_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/${ID}) +echo $VERIFY_DELETE_RESULT | jq '.' + +log_success "测试完成" +``` + +--- + +## 测试报告格式 + +每个模块测试完成后,应输出测试报告: + +``` +=== [模块名] 测试报告 === + +步骤 结果 备注 +------------------------ +新增 [通过/失败] +查询(新增后) [通过/失败] +更新 [通过/失败] +查询(更新后) [通过/失败] +列表查询 [通过/失败] +删除 [通过/失败] +查询(删除后) [通过/失败] + +总体结果: [全部通过/部分失败/全部失败] +``` + +--- + +## Profile 模块测试流程 + +``` +获取 -> 更新 -> 查询(验证) -> 修改密码 +``` + +### 步骤详解 + +### 1. 获取个人信息 +获取当前登录用户的个人信息 + +**验证点:** +- 返回完整用户信息 +- 头像和简介字段正确 + +**示例请求:** +```bash +curl -X GET http://localhost:8888/api/v1/profile/me +``` + +--- + +### 2. 更新个人资料 +更新用户名、手机号、头像、简介等字段 + +**验证点:** +- 字段值正确更新 +- 返回更新后的完整信息 +- 未更新字段值保持不变 + +**示例请求:** +```bash +curl -X PUT http://localhost:8888/api/v1/profile/me \ + -H "Content-Type: application/json" \ + -d '{ + "username": "updated_user", + "phone": "13900139000", + "avatar": "http://example.com/avatar.jpg", + "bio": "个人简介" + }' +``` + +--- + +### 3. 查询个人信息 (验证更新) +再次获取个人信息,验证更新结果 + +**验证点:** +- 更新的字段值正确 +- 未更新的字段值保持不变 + +**示例请求:** +```bash +curl -X GET http://localhost:8888/api/v1/profile/me +``` + +--- + +### 4. 修改密码 +修改用户密码 + +**验证点:** +- 返回正确的成功响应 +- 错误密码返回正确错误信息 + +**示例请求:** +```bash +curl -X POST http://localhost:8888/api/v1/profile/password \ + -H "Content-Type: application/json" \ + -d '{ + "oldPassword": "password123", + "newPassword": "newpassword123" + }' +``` + +**注意:** 修改密码后需要重新登录,完整验证流程需要实现登录接口并获取新 token。 + +--- + +## 测试注意事项 + +1. **环境准备** + - 确保服务已启动 + - 数据库已连接 + - 端口可访问 + +2. **数据隔离** + - 每次测试使用不同的测试数据 + - 避免与其他测试产生冲突 + - 建议使用时间戳或随机数生成唯一数据 + +3. **错误处理** + - 验证错误提示信息正确 + - 验证 HTTP 状态码 + - 验证响应格式 + +4. **边界条件** + - 测试必填字段缺失 + - 测试数据格式错误(如邮箱格式) + - 测试字段长度限制 + - 测试重复数据(唯一性验证) diff --git a/backend/tests/profile/test_profile.sh b/backend/tests/profile/test_profile.sh new file mode 100644 index 0000000..ddeede5 --- /dev/null +++ b/backend/tests/profile/test_profile.sh @@ -0,0 +1,193 @@ +#!/bin/bash + +# Profile 模块测试脚本(含完整认证流程)- 不依赖 jq +# 测试流程: 注册 -> 登录 -> 获取个人信息 -> 更新 -> 查询(验证) -> 修改密码 -> 使用新token验证 + +# 配置 +BASE_URL="http://localhost:8888/api/v1" +MODULE="profile" +TIMESTAMP=$(date +%s) + +# 日志函数 +log_info() { + echo -e "\033[32m[INFO]\033[0m $1" +} + +log_success() { + echo -e "\033[32m[SUCCESS]\033[0m $1" +} + +log_error() { + echo -e "\033[31m[ERROR]\033[0m $1" +} + +log_step() { + echo -e "\n\033[36m--- 步骤 $1: $2 ---\033[0m" +} + +# 存储登录后获取的 Token +AUTH_TOKEN="" + +# 1. 注册新用户 +log_step "1" "注册用户" +REGISTER_RESULT=$(curl -s -X POST ${BASE_URL}/register \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"testuser${TIMESTAMP}\", + \"email\": \"test${TIMESTAMP}@example.com\", + \"password\": \"password123\", + \"phone\": \"13800138000\" + }") + +# 解析注册响应,获取用户ID +REGISTER_ID=$(echo "$REGISTER_RESULT" | grep -o '"id":[^,}]*' | sed 's/"id"://' | tr -d ' ') +if [ -z "$REGISTER_ID" ]; then + log_error "注册失败" + echo "$REGISTER_RESULT" + exit 1 +fi + +log_success "注册成功,ID: $REGISTER_ID" +echo "$REGISTER_RESULT" + +# 2. 登录 +log_step "2" "用户登录" +LOGIN_RESULT=$(curl -s -X POST ${BASE_URL}/login \ + -H "Content-Type: application/json" \ + -d "{ + \"email\": \"test${TIMESTAMP}@example.com\", + \"password\": \"password123\" + }") + +# 解析并存储 Token +AUTH_TOKEN=$(echo "$LOGIN_RESULT" | grep -o '"token":"[^"]*"' | sed 's/"token":"//' | sed 's/"$//') +LOGIN_CODE=$(echo "$LOGIN_RESULT" | grep -o '"code":[^,}]*' | sed 's/"code"://' | tr -d ' ') + +if [ "$LOGIN_CODE" != "200" ] || [ -z "$AUTH_TOKEN" ]; then + log_error "登录失败" + echo "$LOGIN_RESULT" + exit 1 +fi + +log_success "登录成功,Token: ${AUTH_TOKEN:0:20}..." +echo "$LOGIN_RESULT" + +# 3. 获取个人信息(带Token) +log_step "3" "获取个人信息(带Token)" +GET_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/me \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${AUTH_TOKEN}") + +# 验证响应 +ID=$(echo "$GET_RESULT" | grep -o '"id":[^,}]*' | sed 's/"id"://' | tr -d ' ') +if [ -z "$ID" ]; then + log_error "获取失败:可能Token无效" + echo "$GET_RESULT" + exit 1 +fi + +log_success "获取成功,ID: $ID" +echo "$GET_RESULT" + +# 4. 更新个人资料 +log_step "4" "更新个人资料" +UPDATE_RESULT=$(curl -s -X PUT ${BASE_URL}/${MODULE}/me \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d "{ + \"username\": \"testuser${TIMESTAMP}_updated\", + \"phone\": \"13900139000\", + \"avatar\": \"http://example.com/avatar${TIMESTAMP}.jpg\", + \"bio\": \"这是个人简介${TIMESTAMP}\" + }") + +# 验证更新 +VERIFY_USERNAME=$(echo "$UPDATE_RESULT" | grep -o '"username":"[^"]*"' | sed 's/"username":"//' | sed 's/"$//') +EXPECTED_USERNAME="testuser${TIMESTAMP}_updated" +if [ "$VERIFY_USERNAME" != "$EXPECTED_USERNAME" ]; then + log_error "更新失败:用户名未更新" + echo "$UPDATE_RESULT" + exit 1 +fi + +log_success "更新成功" +echo "$UPDATE_RESULT" + +# 5. 查询个人信息(验证更新) +log_step "5" "查询个人信息(验证更新)" +VERIFY_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/me \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${AUTH_TOKEN}") + +VERIFY_USERNAME=$(echo "$VERIFY_RESULT" | grep -o '"username":"[^"]*"' | sed 's/"username":"//' | sed 's/"$//') +if [ "$VERIFY_USERNAME" != "$EXPECTED_USERNAME" ]; then + log_error "验证失败:用户名未更新" + echo "$VERIFY_RESULT" + exit 1 +fi + +log_success "验证成功" + +# 6. 修改密码(使用旧密码) +log_step "6" "修改密码(使用旧密码)" +OLD_PASSWORD_RESULT=$(curl -s -X POST ${BASE_URL}/${MODULE}/password \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d "{ + \"oldPassword\": \"password123\", + \"newPassword\": \"newpassword123\" + }") + +# 验证修改 +OLD_PASSWORD_CODE=$(echo "$OLD_PASSWORD_RESULT" | grep -o '"code":[^,}]*' | sed 's/"code"://' | tr -d ' ') +if [ "$OLD_PASSWORD_CODE" != "200" ]; then + log_error "修改密码失败" + echo "$OLD_PASSWORD_RESULT" + exit 1 +fi + +log_success "修改密码成功" +echo "$OLD_PASSWORD_RESULT" + +# 7. 使用新密码重新登录 +log_step "7" "使用新密码重新登录" +NEW_LOGIN_RESULT=$(curl -s -X POST ${BASE_URL}/login \ + -H "Content-Type: application/json" \ + -d "{ + \"email\": \"test${TIMESTAMP}@example.com\", + \"password\": \"newpassword123\" + }") + +# 获取新Token +NEW_AUTH_TOKEN=$(echo "$NEW_LOGIN_RESULT" | grep -o '"token":"[^"]*"' | sed 's/"token":"//' | sed 's/"$//') +NEW_LOGIN_CODE=$(echo "$NEW_LOGIN_RESULT" | grep -o '"code":[^,}]*' | sed 's/"code"://' | tr -d ' ') + +if [ "$NEW_LOGIN_CODE" != "200" ] || [ -z "$NEW_AUTH_TOKEN" ]; then + log_error "重新登录失败" + echo "$NEW_LOGIN_RESULT" + exit 1 +fi + +log_success "重新登录成功,新Token: ${NEW_AUTH_TOKEN:0:20}..." +echo "$NEW_LOGIN_RESULT" + +# 8. 获取个人信息(验证密码修改成功) +log_step "8" "获取个人信息(验证密码修改成功)" +FINAL_GET_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/me \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${NEW_AUTH_TOKEN}") + +# 验证响应 +ID=$(echo "$FINAL_GET_RESULT" | grep -o '"id":[^,}]*' | sed 's/"id"://' | tr -d ' ') +if [ -z "$ID" ]; then + log_error "获取失败:新Token可能无效" + echo "$FINAL_GET_RESULT" + exit 1 +fi + +log_success "验证成功,密码修改完成" + +echo -e "\n\033[32m=== Profile 模块测试完成 ===\033[0m" +echo "用户ID: $REGISTER_ID" +echo "测试账号: test${TIMESTAMP}@example.com" +echo "测试密码: password123 / newpassword123" diff --git a/backend/tests/profile/test_profile_no_jq.sh b/backend/tests/profile/test_profile_no_jq.sh new file mode 100644 index 0000000..fba3d02 --- /dev/null +++ b/backend/tests/profile/test_profile_no_jq.sh @@ -0,0 +1,207 @@ +#!/bin/bash + +# Profile 模块测试脚本(含完整认证流程)- 不依赖 jq +# 测试流程: 注册 -> 登录 -> 获取个人信息 -> 更新 -> 查询(验证) -> 修改密码 -> 使用新token验证 + +# 日志函数 +log_info() { + echo -e "\033[32m[INFO]\033[0m $1" +} + +log_success() { + echo -e "\033[32m[SUCCESS]\033[0m $1" +} + +log_error() { + echo -e "\033[31m[ERROR]\033[0m $1" +} + +log_step() { + echo -e "\n\033[36m--- 步骤 $1: $2 ---\033[0m" +} + +# 简单 JSON 提取函数 +extract_value() { + local json="$1" + local key="$2" + # 匹配 "key":value 或 "key": "value" + echo "$json" | grep -o "\"$key\":[^,}]*" | sed 's/"'"$key'"://' | sed 's/^"//' | sed 's/"$//' | tr -d ' ' +} + +extract_int() { + local json="$1" + local key="$2" + echo "$json" | grep -o "\"$key\":[^,}]*" | sed 's/"'"$key'"://' | tr -d ' ' +} + +# 配置 +BASE_URL="http://localhost:8888/api/v1" +MODULE="profile" +TIMESTAMP=$(date +%s) + +# 存储登录后获取的 Token +AUTH_TOKEN="" + +# 1. 注册新用户 +log_step "1" "注册用户" +REGISTER_RESULT=$(curl -s -X POST ${BASE_URL}/register \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"testuser${TIMESTAMP}\", + \"email\": \"test${TIMESTAMP}@example.com\", + \"password\": \"password123\", + \"phone\": \"13800138000\" + }") + +# 解析注册响应,获取用户ID +REGISTER_ID=$(extract_int "$REGISTER_RESULT" "id") +if [ -z "$REGISTER_ID" ]; then + log_error "注册失败" + echo "$REGISTER_RESULT" + exit 1 +fi + +log_success "注册成功,ID: $REGISTER_ID" +echo "$REGISTER_RESULT" + +# 2. 登录 +log_step "2" "用户登录" +LOGIN_RESULT=$(curl -s -X POST ${BASE_URL}/login \ + -H "Content-Type: application/json" \ + -d "{ + \"email\": \"test${TIMESTAMP}@example.com\", + \"password\": \"password123\" + }") + +# 解析并存储 Token +AUTH_TOKEN=$(extract_value "$LOGIN_RESULT" "token") +LOGIN_CODE=$(extract_int "$LOGIN_RESULT" "code") + +if [ "$LOGIN_CODE" != "200" ] || [ -z "$AUTH_TOKEN" ]; then + log_error "登录失败" + echo "$LOGIN_RESULT" + exit 1 +fi + +log_success "登录成功,Token: ${AUTH_TOKEN:0:20}..." +echo "$LOGIN_RESULT" + +# 3. 获取个人信息(带Token) +log_step "3" "获取个人信息(带Token)" +GET_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/me \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${AUTH_TOKEN}") + +# 验证响应 +ID=$(extract_int "$GET_RESULT" "id") +if [ -z "$ID" ]; then + log_error "获取失败:可能Token无效" + echo "$GET_RESULT" + exit 1 +fi + +log_success "获取成功,ID: $ID" +echo "$GET_RESULT" + +# 4. 更新个人资料 +log_step "4" "更新个人资料" +UPDATE_RESULT=$(curl -s -X PUT ${BASE_URL}/${MODULE}/me \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d "{ + \"username\": \"testuser${TIMESTAMP}_updated\", + \"phone\": \"13900139000\", + \"avatar\": \"http://example.com/avatar${TIMESTAMP}.jpg\", + \"bio\": \"这是个人简介${TIMESTAMP}\" + }") + +# 验证更新 +VERIFY_USERNAME=$(extract_value "$UPDATE_RESULT" "username") +EXPECTED_USERNAME="testuser${TIMESTAMP}_updated" +if [ "$VERIFY_USERNAME" != "$EXPECTED_USERNAME" ]; then + log_error "更新失败:用户名未更新" + echo "$UPDATE_RESULT" + exit 1 +fi + +log_success "更新成功" +echo "$UPDATE_RESULT" + +# 5. 查询个人信息(验证更新) +log_step "5" "查询个人信息(验证更新)" +VERIFY_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/me \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${AUTH_TOKEN}") + +VERIFY_USERNAME=$(extract_value "$VERIFY_RESULT" "username") +if [ "$VERIFY_USERNAME" != "$EXPECTED_USERNAME" ]; then + log_error "验证失败:用户名未更新" + echo "$VERIFY_RESULT" + exit 1 +fi + +log_success "验证成功" + +# 6. 修改密码(使用旧密码) +log_step "6" "修改密码(使用旧密码)" +OLD_PASSWORD_RESULT=$(curl -s -X POST ${BASE_URL}/${MODULE}/password \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${AUTH_TOKEN}" \ + -d "{ + \"oldPassword\": \"password123\", + \"newPassword\": \"newpassword123\" + }") + +# 验证修改 +OLD_PASSWORD_CODE=$(extract_int "$OLD_PASSWORD_RESULT" "code") +if [ "$OLD_PASSWORD_CODE" != "200" ]; then + log_error "修改密码失败" + echo "$OLD_PASSWORD_RESULT" + exit 1 +fi + +log_success "修改密码成功" +echo "$OLD_PASSWORD_RESULT" + +# 7. 使用新密码重新登录 +log_step "7" "使用新密码重新登录" +NEW_LOGIN_RESULT=$(curl -s -X POST ${BASE_URL}/login \ + -H "Content-Type: application/json" \ + -d "{ + \"email\": \"test${TIMESTAMP}@example.com\", + \"password\": \"newpassword123\" + }") + +# 获取新Token +NEW_AUTH_TOKEN=$(extract_value "$NEW_LOGIN_RESULT" "token") +NEW_LOGIN_CODE=$(extract_int "$NEW_LOGIN_RESULT" "code") + +if [ "$NEW_LOGIN_CODE" != "200" ] || [ -z "$NEW_AUTH_TOKEN" ]; then + log_error "重新登录失败" + echo "$NEW_LOGIN_RESULT" + exit 1 +fi + +log_success "重新登录成功,新Token: ${NEW_AUTH_TOKEN:0:20}..." +echo "$NEW_LOGIN_RESULT" + +# 8. 获取个人信息(验证密码修改成功) +log_step "8" "获取个人信息(验证密码修改成功)" +FINAL_GET_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/me \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${NEW_AUTH_TOKEN}") + +# 验证响应 +ID=$(extract_int "$FINAL_GET_RESULT" "id") +if [ -z "$ID" ]; then + log_error "获取失败:新Token可能无效" + echo "$FINAL_GET_RESULT" + exit 1 +fi + +log_success "验证成功,密码修改完成" + +echo -e "\n\033[32m=== Profile 模块测试完成 ===\033[0m" +echo "用户ID: $REGISTER_ID" +echo "测试账号: test${TIMESTAMP}@example.com" +echo "测试密码: password123 / newpassword123" diff --git a/backend/tests/run_all_tests.sh b/backend/tests/run_all_tests.sh new file mode 100644 index 0000000..0f7a85f --- /dev/null +++ b/backend/tests/run_all_tests.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +# 统一测试运行脚本 + +set -e + +BASE_DIR="$(cd "$(dirname "$0")" && pwd)" +RESULTS_DIR="$BASE_DIR/results" +mkdir -p "$RESULTS_DIR" + +# 日志函数 +log_info() { + echo -e "\033[32m[INFO]\033[0m $1" +} + +log_success() { + echo -e "\033[32m[SUCCESS]\033[0m $1" +} + +log_error() { + echo -e "\033[31m[ERROR]\033[0m $1" +} + +print_header() { + echo -e "\n\033[36m========================================\033[0m" + echo -e "\033[36m $1\033[0m" + echo -e "\033[36m========================================\033[0m\n" +} + +# 测试结果统计 +declare -A USER_RESULTS=() +declare -A PROFILE_RESULTS=() + +# 运行 User 模块测试 +print_header "User 模块测试" +if bash "$BASE_DIR/user/test_user.sh" > "$RESULTS_DIR/user_test.log" 2>&1; then + USER_RESULTS["新增"]="通过" + USER_RESULTS["查询(新增后)"]="通过" + USER_RESULTS["更新"]="通过" + USER_RESULTS["查询(更新后)"]="通过" + USER_RESULTS["列表查询"]="通过" + USER_RESULTS["删除"]="通过" + USER_RESULTS["查询(删除后)"]="通过" + log_success "User 模块测试全部通过" +else + USER_RESULTS["新增"]="失败" + log_error "User 模块测试失败,查看日志: $RESULTS_DIR/user_test.log" +fi + +# 运行 Profile 模块测试 +print_header "Profile 模块测试" +if bash "$BASE_DIR/profile/test_profile.sh" > "$RESULTS_DIR/profile_test.log" 2>&1; then + PROFILE_RESULTS["获取"]="通过" + PROFILE_RESULTS["更新"]="通过" + PROFILE_RESULTS["查询(更新后)"]="通过" + PROFILE_RESULTS["修改密码"]="通过" + log_success "Profile 模块测试全部通过" +else + PROFILE_RESULTS["获取"]="失败" + log_error "Profile 模块测试失败,查看日志: $RESULTS_DIR/profile_test.log" +fi + +# 输出测试报告 +echo -e "\n\033[32m========================================\033[0m" +echo -e "\033[32m 测试报告\033[0m" +echo -e "\033[32m========================================\033[0m\n" + +echo -e "\033[1m【User 模块】\033[0m" +echo "步骤 结果" +echo "--------------------------------" +echo "新增 ${USER_RESULTS["新增"]}" +echo "查询(新增后) ${USER_RESULTS["查询(新增后)"]}" +echo "更新 ${USER_RESULTS["更新"]}" +echo "查询(更新后) ${USER_RESULTS["查询(更新后)"]}" +echo "列表查询 ${USER_RESULTS["列表查询"]}" +echo "删除 ${USER_RESULTS["删除"]}" +echo "查询(删除后) ${USER_RESULTS["查询(删除后)"]}" + +echo -e "\n\033[1m【Profile 模块】\033[0m" +echo "步骤 结果" +echo "--------------------------------" +echo "获取 ${PROFILE_RESULTS["获取"]}" +echo "更新 ${PROFILE_RESULTS["更新"]}" +echo "查询(更新后) ${PROFILE_RESULTS["查询(更新后)"]}" +echo "修改密码 ${PROFILE_RESULTS["修改密码"]}" + +# 判断总体结果 +USER_PASS=0 +USER_FAIL=0 +for result in "${USER_RESULTS[@]}"; do + if [ "$result" = "通过" ]; then + ((USER_PASS++)) + else + ((USER_FAIL++)) + fi +done + +PROFILE_PASS=0 +PROFILE_FAIL=0 +for result in "${PROFILE_RESULTS[@]}"; do + if [ "$result" = "通过" ]; then + ((PROFILE_PASS++)) + else + ((PROFILE_FAIL++)) + fi +done + +echo -e "\n\033[32m========================================\033[0m" +echo -e "\033[32m 总体结果\033[0m" +echo -e "\033[32m========================================\033[0m" + +echo "User 模块: 通过 $USER_PASS / $((USER_PASS + USER_FAIL)) 项" +echo "Profile 模块: 通过 $PROFILE_PASS / $((PROFILE_PASS + PROFILE_FAIL)) 项" + +if [ $USER_FAIL -eq 0 ] && [ $PROFILE_FAIL -eq 0 ]; then + echo -e "\033[32m所有测试通过!\033[0m\n" + exit 0 +else + echo -e "\033[31m存在失败的测试项,请查看日志\033[0m\n" + exit 1 +fi diff --git a/backend/tests/user/test_user.sh b/backend/tests/user/test_user.sh new file mode 100644 index 0000000..7501b84 --- /dev/null +++ b/backend/tests/user/test_user.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +BASE_URL="http://localhost:8888/api/v1" +MODULE="user" +TIMESTAMP=$(date +%s) + +log_step() { + echo -e "\n\033[36m--- Step $1: $2 ---\033[0m" +} + +log_success() { + echo -e "\033[32m[SUCCESS]\033[0m $1" +} + +log_error() { + echo -e "\033[31m[ERROR]\033[0m $1" +} + +extract_value() { + echo "$1" | grep -o "\"$2\":\"[^\"]*\"" | sed 's/"'"$2'""://' | sed 's/^"//' | sed 's/"$//' +} + +extract_int() { + echo "$1" | grep -o "\"$2\":[^,}]*" | sed 's/"'"$2'"://' | tr -d ' ' +} + +AUTH_TOKEN="" + +log_step "0" "Register admin" +REGISTER_RESULT=$(curl -s -X POST ${BASE_URL}/register -H "Content-Type: application/json" -d "{\"username\":\"admin${TIMESTAMP}\",\"email\":\"admin${TIMESTAMP}@example.com\",\"password\":\"password123\",\"phone\":\"13800138000\"}") +log_success "Register success" +echo "$REGISTER_RESULT" + +log_step "0.1" "Admin login" +LOGIN_RESULT=$(curl -s -X POST ${BASE_URL}/login -H "Content-Type: application/json" -d "{\"email\":\"admin${TIMESTAMP}@example.com\",\"password\":\"password123\"}") +AUTH_TOKEN=$(extract_value "$LOGIN_RESULT" "token") +LOGIN_CODE=$(extract_int "$LOGIN_RESULT" "code") + +if [ "$LOGIN_CODE" != "200" ] || [ -z "$AUTH_TOKEN" ]; then + log_error "Login failed" + echo "$LOGIN_RESULT" + exit 1 +fi + +log_success "Login success" +echo "$LOGIN_RESULT" + +log_step "1" "Create user" +CREATE_RESULT=$(curl -s -X POST ${BASE_URL}/${MODULE} -H "Content-Type: application/json" -H "Authorization: Bearer ${AUTH_TOKEN}" -d "{\"username\":\"testuser${TIMESTAMP}\",\"email\":\"test${TIMESTAMP}@example.com\",\"password\":\"password123\",\"phone\":\"13800138000\"}") +ID=$(extract_int "$CREATE_RESULT" "id") + +if [ -z "$ID" ]; then + log_error "Create failed" + echo "$CREATE_RESULT" + exit 1 +fi + +log_success "Create success, ID: $ID" +echo "$CREATE_RESULT" + +log_step "2" "Query user" +QUERY_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/${ID} -H "Authorization: Bearer ${AUTH_TOKEN}") +QUERY_ID=$(extract_int "$QUERY_RESULT" "id") + +if [ "$QUERY_ID" != "$ID" ]; then + log_error "Query failed" + echo "$QUERY_RESULT" + exit 1 +fi + +log_success "Query success" +echo "$QUERY_RESULT" + +log_step "3" "Update user" +UPDATE_RESULT=$(curl -s -X PUT ${BASE_URL}/${MODULE}/${ID} -H "Content-Type: application/json" -H "Authorization: Bearer ${AUTH_TOKEN}" -d "{\"username\":\"testuser${TIMESTAMP}_updated\",\"phone\":\"13900139000\"}") +VERIFY_UPDATE_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/${ID} -H "Authorization: Bearer ${AUTH_TOKEN}") +VERIFY_USERNAME=$(extract_value "$VERIFY_UPDATE_RESULT" "username") +VERIFY_PHONE=$(extract_value "$VERIFY_UPDATE_RESULT" "phone") +EXPECTED_USERNAME="testuser${TIMESTAMP}_updated" + +if [ "$VERIFY_USERNAME" != "$EXPECTED_USERNAME" ] || [ "$VERIFY_PHONE" != "13900139000" ]; then + log_error "Update failed" + echo "$VERIFY_UPDATE_RESULT" + exit 1 +fi + +log_success "Update success" + +log_step "4" "Verify update" +VERIFY_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/${ID} -H "Authorization: Bearer ${AUTH_TOKEN}") +VERIFY_USERNAME=$(extract_value "$VERIFY_RESULT" "username") + +if [ "$VERIFY_USERNAME" != "$EXPECTED_USERNAME" ]; then + log_error "Verify failed" + echo "$VERIFY_RESULT" + exit 1 +fi + +log_success "Verify success" + +log_step "5" "List query" +LIST_RESULT=$(curl -s -X GET "${BASE_URL}/${MODULE}s?page=1&pageSize=10" -H "Authorization: Bearer ${AUTH_TOKEN}") + +if echo "$LIST_RESULT" | grep -q '"id"'; then + log_success "List success" +else + log_error "List failed" + echo "$LIST_RESULT" + exit 1 +fi + +log_step "6" "Delete user" +DELETE_RESULT=$(curl -s -X DELETE ${BASE_URL}/${MODULE}/${ID} -H "Authorization: Bearer ${AUTH_TOKEN}") +DELETE_CODE=$(extract_int "$DELETE_RESULT" "code") + +if [ "$DELETE_CODE" = "200" ]; then + log_success "Delete success" +else + log_error "Delete failed" + echo "$DELETE_RESULT" + exit 1 +fi + +log_step "7" "Verify delete" +VERIFY_DELETE_RESULT=$(curl -s -X GET ${BASE_URL}/${MODULE}/${ID} -H "Authorization: Bearer ${AUTH_TOKEN}") +VERIFY_CODE=$(extract_int "$VERIFY_DELETE_RESULT" "code") + +if [ "$VERIFY_CODE" = "404" ]; then + log_success "Verify success" +else + log_error "Verify failed" + echo "$VERIFY_DELETE_RESULT" + exit 1 +fi + +echo -e "\n\033[32m=== User module test completed ===\033[0m" diff --git a/backend/zero-skills.md b/backend/zero-skills.md new file mode 100644 index 0000000..9202f35 --- /dev/null +++ b/backend/zero-skills.md @@ -0,0 +1,122 @@ +# Zero Skills - AI 技能框架 + +> zero-skills 是基于 go-zero 的 AI 技能管理框架,用于创建和管理 AI 助手的自定义技能。 + +## 官方资源 + +- [GitHub](https://github.com/zeromicro/zero-skills) + +## 概述 + +zero-skills 提供了一套技能定义和执行的框架,使得可以方便地为 AI 系统添加自定义功能和业务逻辑。 + +## 核心概念 + +### 技能 (Skill) +技能是 AI 系统可以执行的一个原子性任务或功能,例如: +- 发送邮件 +- 调用 API +- 数据库查询 +- 文件处理 + +### 技能定义 + +```go +type Skill struct { + Name string // 技能名称 + Description string // 技能描述 + Parameters map[string]interface{} // 参数定义 + Handler HandlerFunc // 处理函数 +} + +type HandlerFunc func(ctx context.Context, args map[string]interface{}) (interface{}, error) +``` + +## 使用示例 + +### 定义技能 + +```go +package skills + +import ( + "context" + "fmt" +) + +var SendEmailSkill = &Skill{ + Name: "send_email", + Description: "Send an email to a recipient", + Parameters: map[string]interface{}{ + "to": map[string]string{"type": "string", "description": "Recipient email"}, + "subject": map[string]string{"type": "string", "description": "Email subject"}, + "body": map[string]string{"type": "string", "description": "Email body"}, + }, + Handler: func(ctx context.Context, args map[string]interface{}) (interface{}, error) { + to := args["to"].(string) + subject := args["subject"].(string) + body := args["body"].(string) + + // 发送邮件逻辑 + fmt.Printf("Sending email to %s: %s\n", to, subject) + + return map[string]interface{}{ + "success": true, + "message": "Email sent successfully", + }, nil + }, +} +``` + +### 注册技能 + +```go +package main + +import ( + "github.com/yourapp/skills" +) + +func main() { + // 初始化技能管理器 + skillManager := skills.NewManager() + + // 注册技能 + skillManager.Register(skills.SendEmailSkill) + skillManager.Register(skills.QueryDatabaseSkill) + + // 启动服务 + // ... +} +``` + +## 技能配置 + +```yaml +skills: + enabled: true + base_path: "./skills" + skills: + - name: "send_email" + enabled: true + config: + smtp_host: "smtp.example.com" + smtp_port: 587 + - name: "query_database" + enabled: true + config: + connection_string: "user:pass@localhost:3306/db" +``` + +## 中间件支持 + +```go +// 添加日志中间件 +skillManager.Use(middleware.Logger()) + +// 添加认证中间件 +skillManager.Use(middleware.Auth()) + +// 添加限流中间件 +skillManager.Use(middleware.RateLimit(100)) +``` diff --git a/frontend/react-native/ai-context.md b/frontend/react-native/ai-context.md new file mode 100644 index 0000000..f98cbd5 --- /dev/null +++ b/frontend/react-native/ai-context.md @@ -0,0 +1,257 @@ +# React Native AI Context - shadcn/ui x NativeWind + +> React Native 应用的 AI 上下文指南,结合 shadcn/ui 设计理念和 NativeWind 工具类。 + +## 概述 + +这是 React Native 应用的 AI 开发上下文,使用 NativeWind (Tailwind CSS for React Native) 作为样式解决方案,遵循 shadcn/ui 的设计理念。 + +## 核心技术栈 + +### 基础框架 +- [Expo](https://expo.dev/) - React Native 开发框架 +- [NativeWind](https://www.nativewind.dev/) - Tailwind CSS for React Native +- [React Native Reusables](https://rnr-docs.vercel.app/) - UI 组件库 + +### shadcn/ui 设计原则 + +1. **Open Code** - 代码完全可定制 +2. **Composition** - 组件可组合 +3. **Distribution** - 易于分发和安装 +4. **Beautiful Defaults** - 美观的默认样式 +5. **AI-Ready** - 支持AI开发 + +## 安装 + +```bash +# 初始化 Expo 项目 +npx create-expo-app myapp --template expo-template-blank-typescript +cd myapp + +# 安装 NativeWind +npm install --save-dev tailwindcss +npm install nativewind +npm install -D @types/react-native + +# 配置 NativeWind +npx tailwindcss init +``` + +## 配置 + +### tailwind.config.js +```javascript +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./App.{js,jsx,ts,tsx}", + "./src/**/*.{js,jsx,ts,tsx}", + ], + presets: [require("nativewind/preset")], + theme: { + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + }, + }, + plugins: [], +} +``` + +### nativewind-env.d.ts +```typescript +/// +``` + +### babel.config.js +```javascript +module.exports = function (api) { + api.cache(true); + return { + presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }]], + plugins: [ + 'react-native-reanimated/plugin', + 'nativewind/babel', + ], + }; +} +``` + +## 核心组件 + +### Button 组件 +```typescript +import { Text, Pressable, PressableProps, ViewProps, TextProps } from 'react-native'; +import { cn } from '@/lib/utils'; + +interface ButtonProps extends PressableProps { + variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost'; + size?: 'default' | 'sm' | 'lg'; + className?: string; + children: React.ReactNode; +} + +const Button = React.forwardRef( + ({ className, variant = 'default', size = 'default', children, ...props }, ref) => { + const variants = { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + }; + + const sizes = { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + }; + + return ( + + {children} + + ); + } +); +``` + +### Card 组件 +```typescript +import { View, ViewProps, Text, TextProps } from 'react-native'; +import { cn } from '@/lib/utils'; + +interface CardProps extends ViewProps { + className?: string; +} + +const Card = React.forwardRef(({ className, ...props }, ref) => ( + +)); + +const CardHeader = React.forwardRef(({ className, ...props }, ref) => ( + +)); + +const CardTitle = React.forwardRef(({ className, ...props }, ref) => ( + +)); + +const CardContent = React.forwardRef(({ className, ...props }, ref) => ( + +)); +``` + +## 设计令牌 + +### 颜色系统 +```typescript +// 使用 CSS 变量定义颜色 +// 在 tailwind.config.js 中配置 +``` + +### 字体系统 +```typescript +// NativeWind 中的字体设置 +// 使用 Tailwind 的字体工具类 +text-sm // 小号文本 +text-base // 基础文本 +text-lg // 大号文本 +font-medium // 中等字重 +font-bold // 粗体 +``` + +## 开发规范 + +### 组件结构 +``` +src/ +├── components/ +│ ├── ui/ # 基础 UI 组件 +│ │ ├── button.tsx +│ │ ├── card.tsx +│ │ └── ... +│ └── features/ # 功能组件 +├── lib/ +│ └── utils.ts # 工具函数 +├── hooks/ # 自定义 hooks +└── screens/ # 屏幕组件 +``` + +### 命名约定 +- 组件使用 PascalCase: `Button.tsx` +- 文件使用 kebab-case: `user-profile.tsx` +- 样式类使用 camelCase 或 Tailwind 工具类 + +## 与 React Web shadcn/ui 对应 + +| React Web | React Native | +|----------|--------------| +| `