From 4f82758fe239af0d9070fbc97d52a146f8945dd0 Mon Sep 17 00:00:00 2001 From: veyvin Date: Sun, 26 Jul 2026 13:36:43 +0000 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=E6=89=A7=E8=A1=8C=20Release=20?= =?UTF-8?q?=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- .github/workflows/release.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78ae5f9..77c5977 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,12 @@ name: Release -run-name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }} +run-name: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || (inputs.version || 'Manual Release') }} on: workflow_dispatch: + inputs: + version: + description: 'Release version (e.g., v1.0.0). If empty, uses package.json version' + required: false push: tags: - 'v*' @@ -12,7 +16,7 @@ permissions: jobs: publish-tauri: - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' strategy: fail-fast: false matrix: @@ -82,7 +86,7 @@ jobs: run: pnpm install - name: Create Release - if: startsWith(github.ref, 'refs/tags/v') && matrix.platform == 'macos-14' + if: (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && matrix.platform == 'macos-14' id: create_release uses: softprops/action-gh-release@v1 env: @@ -107,7 +111,7 @@ jobs: includeUpdaterJson: true publish-webview2-fixed: - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' strategy: fail-fast: false matrix: From 799751a1c3a6a9a24a3ba8c23f9ea002e398d8cc Mon Sep 17 00:00:00 2001 From: veyvin Date: Sun, 26 Jul 2026 13:57:21 +0000 Subject: [PATCH 2/8] feat: GoReleaser Git Tag Error Co-authored-by: traeagent --- .github/workflows/release.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 77c5977..cc95a66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,8 @@ jobs: name: Build ${{ matrix.display_name }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v3 @@ -67,6 +69,18 @@ jobs: VERSION=$(node -p "require('./package.json').version") echo "version=v$VERSION" >> $GITHUB_OUTPUT + - name: Ensure git tag exists locally + shell: bash + run: | + VERSION="${{ matrix.platform == 'windows-latest' && steps.get_version_windows.outputs.version || steps.get_version_unix.outputs.version }}" + git fetch --tags --depth=1 origin "refs/tags/${VERSION}:refs/tags/${VERSION}" 2>/dev/null || true + if ! git rev-parse "$VERSION" >/dev/null 2>&1; then + git tag "$VERSION" + echo "Created local tag $VERSION" + else + echo "Tag $VERSION already exists" + fi + - name: install Rust stable uses: dtolnay/rust-toolchain@stable with: @@ -129,6 +143,8 @@ jobs: name: Build ${{ matrix.display_name }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v3 @@ -149,6 +165,19 @@ jobs: $VERSION = (node -p "require('./package.json').version") echo "version=v$VERSION" >> $env:GITHUB_OUTPUT + - name: Ensure git tag exists locally + shell: pwsh + run: | + $version = "${{ steps.get_version.outputs.version }}" + git fetch --tags --depth=1 origin "refs/tags/${version}:refs/tags/${version}" 2>$null + $null = git rev-parse "$version" 2>$null + if ($LASTEXITCODE -ne 0) { + git tag "$version" + Write-Host "Created local tag $version" + } else { + Write-Host "Tag $version already exists" + } + - name: install Rust stable uses: dtolnay/rust-toolchain@stable with: From ddb1b71a6f27120e1192852593bc67a32771d451 Mon Sep 17 00:00:00 2001 From: veyvin Date: Sun, 26 Jul 2026 14:14:42 +0000 Subject: [PATCH 3/8] feat: Fix Node.js and pnpm Compatibility Co-authored-by: traeagent --- .github/workflows/dev.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 1a7d411..a3c4955 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -91,7 +91,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'pnpm' - name: install Rust stable diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc95a66..2140b71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'pnpm' - name: Get Version (Windows) @@ -155,7 +155,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'pnpm' - name: Get Version From 3e2ff6c48e9b44674a3659489f243b1b69fd7a33 Mon Sep 17 00:00:00 2001 From: veyvin Date: Mon, 27 Jul 2026 13:59:10 +0800 Subject: [PATCH 4/8] fix(ci): pin pnpm 9 to avoid ERR_PNPM_IGNORED_BUILDS on release pnpm latest (11) ignores dependency build scripts by default, which fails husky prepare during install. Also disable husky in CI and whitelist native build deps for future pnpm 10+. Co-authored-by: Cursor --- .github/workflows/dev.yml | 5 ++++- .github/workflows/release.yml | 10 ++++++++-- package.json | 9 +++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index a3c4955..5105209 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -85,7 +85,8 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v3 with: - version: latest + # pnpm 10+ ignores dependency build scripts by default (ERR_PNPM_IGNORED_BUILDS) + version: 9 run_install: false - name: Setup Node.js @@ -111,6 +112,8 @@ jobs: - name: Install dependencies run: pnpm install + env: + HUSKY: '0' - name: Build Tauri App (macOS) if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-14' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2140b71..5405740 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,8 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v3 with: - version: latest + # pnpm 10+ ignores dependency build scripts by default (ERR_PNPM_IGNORED_BUILDS) + version: 9 run_install: false - name: Setup Node.js @@ -98,6 +99,8 @@ jobs: - name: Install dependencies run: pnpm install + env: + HUSKY: '0' - name: Create Release if: (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && matrix.platform == 'macos-14' @@ -149,7 +152,8 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v3 with: - version: latest + # pnpm 10+ ignores dependency build scripts by default (ERR_PNPM_IGNORED_BUILDS) + version: 9 run_install: false - name: Setup Node.js @@ -190,6 +194,8 @@ jobs: - name: Install dependencies run: pnpm install + env: + HUSKY: '0' - name: Prepare WebView2 Fixed Runtime shell: pwsh diff --git a/package.json b/package.json index 7ca04a0..69d6cb7 100644 --- a/package.json +++ b/package.json @@ -71,5 +71,14 @@ "eslint --fix", "prettier --write" ] + }, + "pnpm": { + "onlyBuiltDependencies": [ + "@parcel/watcher", + "esbuild", + "optipng-bin", + "sharp", + "zopflipng-bin" + ] } } From d34399d6159bd44f6ccd2e4fdeaa9754baf3916f Mon Sep 17 00:00:00 2001 From: veyvin Date: Sun, 2 Aug 2026 17:20:32 +0800 Subject: [PATCH 5/8] ci: add TAURI_UPDATER_PRIVATE_KEY env for tauri-action Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5405740..edfdef0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,6 +119,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + TAURI_UPDATER_PRIVATE_KEY: ${{ secrets.TAURI_UPDATER_PRIVATE_KEY }} + TAURI_UPDATER_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_UPDATER_PRIVATE_KEY_PASSWORD }} with: tagName: ${{ matrix.platform == 'windows-latest' && steps.get_version_windows.outputs.version || steps.get_version_unix.outputs.version }} releaseName: ${{ matrix.platform == 'windows-latest' && steps.get_version_windows.outputs.version || steps.get_version_unix.outputs.version }} @@ -252,6 +254,8 @@ jobs: NODE_OPTIONS: "--max_old_space_size=4096" TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + TAURI_UPDATER_PRIVATE_KEY: ${{ secrets.TAURI_UPDATER_PRIVATE_KEY }} + TAURI_UPDATER_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_UPDATER_PRIVATE_KEY_PASSWORD }} with: configPath: src-tauri/tauri.windows.conf.json args: --target ${{ matrix.target }} From d3427d0a1c83d96a8c833e1c3c7041db10a9f162 Mon Sep 17 00:00:00 2001 From: veyvin Date: Wed, 5 Aug 2026 05:45:09 +0000 Subject: [PATCH 6/8] feat: Tauri Build Failed: Secret Key Error Co-authored-by: traeagent --- .github/workflows/release.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index edfdef0..5405740 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,8 +119,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - TAURI_UPDATER_PRIVATE_KEY: ${{ secrets.TAURI_UPDATER_PRIVATE_KEY }} - TAURI_UPDATER_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_UPDATER_PRIVATE_KEY_PASSWORD }} with: tagName: ${{ matrix.platform == 'windows-latest' && steps.get_version_windows.outputs.version || steps.get_version_unix.outputs.version }} releaseName: ${{ matrix.platform == 'windows-latest' && steps.get_version_windows.outputs.version || steps.get_version_unix.outputs.version }} @@ -254,8 +252,6 @@ jobs: NODE_OPTIONS: "--max_old_space_size=4096" TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - TAURI_UPDATER_PRIVATE_KEY: ${{ secrets.TAURI_UPDATER_PRIVATE_KEY }} - TAURI_UPDATER_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_UPDATER_PRIVATE_KEY_PASSWORD }} with: configPath: src-tauri/tauri.windows.conf.json args: --target ${{ matrix.target }} From bc3c8abfc8dea54dedf15a3da1e3f28ae87bd6e7 Mon Sep 17 00:00:00 2001 From: veyvin Date: Wed, 5 Aug 2026 07:53:01 +0000 Subject: [PATCH 7/8] feat: Tauri Updater Private Key Error Co-authored-by: traeagent --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index edd3058..07a5dcb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -91,7 +91,7 @@ }, "updater": { "active": true, - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEM5NjUxODYwQkUzMkY2RTYKUldUbTlqSytZQmhseVIvWXg0c1RLakxCajBpdGQzeGxUbVMyaGYyRWZXdXdvNURtTW5lZXBVVWQK", + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDNBOTY3NDkyRkYzM0FCRQpSV1MrT3ZNdlNXZXBBNXhiNC91V0JGMjFVSkYrVXRTZk9zTS9vNlFXWXBZbUVNSklvbHZkV1A4Lwo=", "endpoints": [ "https://vip.123pan.cn/1840147130/updater/latest.json", "https://github.com/Cloxl/CursorPool_Client/releases/latest/download/latest.json", From d31eeeb2c8f828c76825062bdc86998e32dff250 Mon Sep 17 00:00:00 2001 From: veyvin Date: Wed, 5 Aug 2026 09:48:48 +0000 Subject: [PATCH 8/8] feat: Tauri Updater Private Key Error Co-authored-by: traeagent --- server/.gitignore | 21 ++++ server/config/config.go | 37 +++++++ server/database/db.go | 121 +++++++++++++++++++++ server/go.mod | 42 ++++++++ server/go.sum | 101 ++++++++++++++++++ server/handlers/accountpool.go | 97 +++++++++++++++++ server/handlers/auth.go | 184 ++++++++++++++++++++++++++++++++ server/handlers/system.go | 112 +++++++++++++++++++ server/handlers/user.go | 124 +++++++++++++++++++++ server/main.go | 39 +++++++ server/middleware/middleware.go | 86 +++++++++++++++ server/models/models.go | 77 +++++++++++++ server/routes/routes.go | 40 +++++++ server/utils/response.go | 45 ++++++++ 14 files changed, 1126 insertions(+) create mode 100644 server/.gitignore create mode 100644 server/config/config.go create mode 100644 server/database/db.go create mode 100644 server/go.mod create mode 100644 server/go.sum create mode 100644 server/handlers/accountpool.go create mode 100644 server/handlers/auth.go create mode 100644 server/handlers/system.go create mode 100644 server/handlers/user.go create mode 100644 server/main.go create mode 100644 server/middleware/middleware.go create mode 100644 server/models/models.go create mode 100644 server/routes/routes.go create mode 100644 server/utils/response.go diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..bf3e3ba --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,21 @@ +# 编译产物 +cursorpool-server +*.exe +*.dll +*.so +*.dylib + +# 数据库文件 +data/ +*.db +*.db-journal +*.sqlite +*.sqlite3 + +# IDE +.idea/ +.vscode/ +*.swp + +# 日志 +*.log diff --git a/server/config/config.go b/server/config/config.go new file mode 100644 index 0000000..bda964f --- /dev/null +++ b/server/config/config.go @@ -0,0 +1,37 @@ +package config + +import "os" + +// Config 全局配置 +type Config struct { + Port string + JWTSecret string + DBPath string + APIPrefix string // 路由前缀,客户端请求 /api/xxx,这里反代去掉 /api + SMTPHost string + SMTPPort string + SMTPUser string + SMTPPass string +} + +var App Config + +func Load() { + App = Config{ + Port: getEnv("PORT", "8787"), + JWTSecret: getEnv("JWT_SECRET", "cursor-pool-demo-secret-change-me"), + DBPath: getEnv("DB_PATH", "./data/cursorpool.db"), + APIPrefix: getEnv("API_PREFIX", ""), // 直接挂在根路径,如需 /api 前缀由反代处理 + SMTPHost: getEnv("SMTP_HOST", ""), + SMTPPort: getEnv("SMTP_PORT", "465"), + SMTPUser: getEnv("SMTP_USER", ""), + SMTPPass: getEnv("SMTP_PASS", ""), + } +} + +func getEnv(key, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} diff --git a/server/database/db.go b/server/database/db.go new file mode 100644 index 0000000..81f3024 --- /dev/null +++ b/server/database/db.go @@ -0,0 +1,121 @@ +package database + +import ( + "log" + "os" + "path/filepath" + "time" + + "cursorpool-server/config" + "cursorpool-server/models" + + "golang.org/x/crypto/bcrypt" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var DB *gorm.DB + +// Init 初始化数据库并自动建表 + 写入种子数据 +func Init() { + // 确保 data 目录存在 + if dir := filepath.Dir(config.App.DBPath); dir != "" { + if err := os.MkdirAll(dir, 0755); err != nil { + log.Fatalf("创建数据库目录失败: %v", err) + } + } + + db, err := gorm.Open(sqlite.Open(config.App.DBPath), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Warn), + }) + if err != nil { + log.Fatalf("连接数据库失败: %v", err) + } + DB = db + + // 自动建表(对齐客户端 types.rs 字段) + if err := db.AutoMigrate( + &models.User{}, + &models.CursorAccount{}, + &models.ActivationCode{}, + &models.Article{}, + &models.PublicInfo{}, + ); err != nil { + log.Fatalf("自动建表失败: %v", err) + } + + seed(db) +} + +// seed 写入演示数据,便于首次启动直接测试 +func seed(db *gorm.DB) { + // 默认账号(密码 123456) + var userCount int64 + db.Model(&models.User{}).Count(&userCount) + if userCount == 0 { + hash, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost) + db.Create(&models.User{ + Email: "demo@cursorpool.test", + Password: string(hash), + Username: "demo", + Level: 1, + TotalCount: 100, + ExpireTime: time.Now().Add(365 * 24 * time.Hour), + }) + } + + // 默认 Cursor 账户池 + var accCount int64 + db.Model(&models.CursorAccount{}).Count(&accCount) + if accCount == 0 { + db.Create(&models.CursorAccount{ + Email: "cursor-account-1@example.com", + Password: "dummy", + Token: "demo-cursor-token-1", + UsageCount: 0, + Status: 1, + }) + } + + // 默认激活码 + var codeCount int64 + db.Model(&models.ActivationCode{}).Count(&codeCount) + if codeCount == 0 { + db.Create(&models.ActivationCode{ + Code: "DEMO2024", + Type: 1, + Name: "体验码", + Level: 1, + Duration: 30, + MaxUses: 1000, + UsedCount: 0, + Status: 1, + }) + } + + // 默认公告 + var artCount int64 + db.Model(&models.Article{}).Count(&artCount) + if artCount == 0 { + db.Create(&models.Article{ + Title: "欢迎使用 Cursor Pool", + Content: "这是后端演示数据。请根据业务需求修改 handlers 目录下的实现。", + }) + } + + // 默认公告信息 + var pubCount int64 + db.Model(&models.PublicInfo{}).Count(&pubCount) + if pubCount == 0 { + db.Create(&models.PublicInfo{ + Type: "info", + Closeable: true, + Title: "系统公告", + Description: "Cursor Pool 后端服务已启动", + ActionText: "前往官网", + ActionURL: "https://pool.52ai.org", + ActionType: "link", + }) + } +} diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..5b0a859 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,42 @@ +module cursorpool-server + +go 1.21 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + golang.org/x/crypto v0.27.0 + gorm.io/driver/sqlite v1.5.6 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..a048b5b --- /dev/null +++ b/server/go.sum @@ -0,0 +1,101 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +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/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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= +gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/server/handlers/accountpool.go b/server/handlers/accountpool.go new file mode 100644 index 0000000..8ff2d44 --- /dev/null +++ b/server/handlers/accountpool.go @@ -0,0 +1,97 @@ +package handlers + +import ( + "time" + + "cursorpool-server/database" + "cursorpool-server/models" + "cursorpool-server/utils" + + "github.com/gin-gonic/gin" +) + +// GetAccount 获取 Cursor 账户池中的账户 +// GET /accountpool/get query: account?, usage_count? (需 JWT) +// 对齐客户端 endpoints.rs get_account +// 返回 AccountPoolInfo {success, account_info, activation_code} (types.rs:266-293) +// +// 核心业务:从池子里分配一个可用 Cursor 账号给当前用户 +// 这里实现一个简单的轮询分配策略,实际可按业务调整(按额度/按权重等) +func GetAccount(c *gin.Context) { + account := c.Query("account") + _ = c.Query("usage_count") // 客户端可选参数,当前分配逻辑未使用 + + var acc models.CursorAccount + var err error + + if account != "" { + // 指定账号查询 + err = database.DB.Where("email = ? AND status = 1", account).First(&acc).Error + } else { + // 分配一个可用账号(取 usage_count 最小的,实现负载均衡) + err = database.DB.Where("status = 1").Order("usage_count ASC, id ASC").First(&acc).Error + } + + if err != nil { + utils.Fail(c, "暂无可用账户") + return + } + + // 更新分配时间和使用次数 + now := time.Now() + database.DB.Model(&acc).Updates(map[string]interface{}{ + "distributed_time": now, + "usage_count": acc.UsageCount + 1, + "update_time": now, + }) + + // 查当前用户的激活码信息(用于返回 activation_code) + userID := c.GetInt("user_id") + var user models.User + database.DB.First(&user, userID) + + // 组装 AccountInfo (对齐 types.rs:62-86) + accountInfo := gin.H{ + "id": acc.ID, + "account": acc.Email, + "password": acc.Password, + "token": acc.Token, + "usage_count": acc.UsageCount + 1, + "status": acc.Status, + "create_time": acc.CreateTime.Format("2006-01-02 15:04:05"), + "distributed_time": now.Format("2006-01-02 15:04:05"), + "update_time": now.Format("2006-01-02 15:04:05"), + } + + // 组装 ActivationCode (对齐 types.rs:277-293) + // 这里返回一个默认的激活码信息,实际应根据用户已激活的码返回 + activationCode := gin.H{ + "id": 0, + "code": "", + "type": 1, + "name": "", + "level": user.Level, + "duration": 0, + "max_uses": 0, + "used_count": 0, + "status": 0, + "notes": "", + "activated_at": "", + "expired_at": user.ExpireTime.Format("2006-01-02 15:04:05"), + } + + utils.Success(c, "获取成功", gin.H{ + "success": true, + "account_info": accountInfo, + "activation_code": activationCode, + }) +} + +// GetUsage 获取 Cursor 使用情况(转发 Cursor 官方 API) +// GET /cursor/usage query: token (需 JWT) +// 对齐客户端 endpoints.rs get_usage +// 注意:客户端实际是直接请求 https://www.cursor.com/api/usage, +// 这里提供后端代理版本(可选),用 cookie 透传 +func GetUsage(c *gin.Context) { + utils.Fail(c, "客户端直接请求 cursor.com,后端无需实现此接口。如需代理请参考 endpoints.rs get_usage") +} diff --git a/server/handlers/auth.go b/server/handlers/auth.go new file mode 100644 index 0000000..3e92362 --- /dev/null +++ b/server/handlers/auth.go @@ -0,0 +1,184 @@ +package handlers + +import ( + "fmt" + "time" + + "cursorpool-server/database" + "cursorpool-server/middleware" + "cursorpool-server/models" + "cursorpool-server/utils" + + "github.com/gin-gonic/gin" + "golang.org/x/crypto/bcrypt" +) + +// CheckUser 检查用户是否存在 +// POST /checkUser form: email +// 对齐客户端 endpoints.rs check_user +func CheckUser(c *gin.Context) { + email := c.PostForm("email") + if email == "" { + utils.Fail(c, "邮箱不能为空") + return + } + + var count int64 + database.DB.Model(&models.User{}).Where("email = ?", email).Count(&count) + + utils.Success(c, "查询成功", gin.H{ + "exists": count > 0, + }) +} + +// SendCode 发送邮箱验证码 +// POST /register/sendEmailCode form: email, type(register|reset) +// 对齐客户端 endpoints.rs send_code +func SendCode(c *gin.Context) { + email := c.PostForm("email") + typ := c.PostForm("type") + if email == "" { + utils.Fail(c, "邮箱不能为空") + return + } + + // TODO: 接入真实 SMTP 发送验证码,并把验证码存 Redis(5分钟过期) + // 这里仅返回成功,验证码演示用 123456 + fmt.Printf("[MAIL] 验证码发送到 %s (type=%s): 123456\n", email, typ) + + utils.Success(c, "验证码已发送", nil) +} + +// Register 邮箱注册 +// POST /emailRegister multipart: email, code, password, spread +// 对齐客户端 endpoints.rs register +func Register(c *gin.Context) { + email := c.PostForm("email") + code := c.PostForm("code") + password := c.PostForm("password") + spread := c.PostForm("spread") + + if email == "" || code == "" || password == "" { + utils.Fail(c, "参数不完整") + return + } + + // TODO: 校验验证码(从 Redis 读) + // if code != "123456" { utils.Fail(c, "验证码错误"); return } + + // 检查是否已注册 + var exists int64 + database.DB.Model(&models.User{}).Where("email = ?", email).Count(&exists) + if exists > 0 { + utils.Fail(c, "该邮箱已注册") + return + } + + // 加密密码 + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + utils.Fail(c, "密码加密失败") + return + } + + user := models.User{ + Email: email, + Password: string(hash), + Username: email, + Level: 0, + TotalCount: 0, + ExpireTime: time.Now(), + Spread: spread, + } + if err := database.DB.Create(&user).Error; err != nil { + utils.Fail(c, "注册失败: "+err.Error()) + return + } + + // 生成 JWT + token, _ := middleware.GenerateToken(user.ID, user.Email) + + utils.Success(c, "注册成功", gin.H{ + "token": token, + "expires_time": time.Now().Add(365 * 24 * time.Hour).Unix(), + }) +} + +// Login 用户登录 +// POST /login form: account, password, spread +// 对齐客户端 endpoints.rs login +// 返回 LoginResponse {token, user_info} (types.rs:23-29) +func Login(c *gin.Context) { + account := c.PostForm("account") + password := c.PostForm("password") + spread := c.PostForm("spread") + _ = spread // 邀请码,暂未使用 + + if account == "" || password == "" { + utils.Fail(c, "账号或密码不能为空") + return + } + + var user models.User + if err := database.DB.Where("email = ?", account).First(&user).Error; err != nil { + utils.Fail(c, "账号不存在") + return + } + + // 校验密码 + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + utils.Fail(c, "密码错误") + return + } + + // 生成 JWT + token, _ := middleware.GenerateToken(user.ID, user.Email) + + // 组装 UserInfo (对齐 types.rs:32-59) + userInfo := gin.H{ + "totalCount": user.TotalCount, + "usedCount": user.UsedCount, + "expireTime": user.ExpireTime.Format("2006-01-02 15:04:05"), + "level": user.Level, + "isExpired": user.ExpireTime.Before(time.Now()), + "username": user.Username, + "code_level": user.CodeLevel, + "code_status": user.CodeStatus, + } + + utils.Success(c, "登录成功", gin.H{ + "token": token, + "user_info": userInfo, + }) +} + +// ResetPassword 重置密码 +// POST /emailResetPassword form: email, code, password +// 对齐客户端 endpoints.rs reset_password +func ResetPassword(c *gin.Context) { + email := c.PostForm("email") + code := c.PostForm("code") + password := c.PostForm("password") + + if email == "" || code == "" || password == "" { + utils.Fail(c, "参数不完整") + return + } + + // TODO: 校验验证码 + // if code != "123456" { utils.Fail(c, "验证码错误"); return } + + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + utils.Fail(c, "密码加密失败") + return + } + + result := database.DB.Model(&models.User{}).Where("email = ?", email).Update("password", string(hash)) + if result.RowsAffected == 0 { + utils.Fail(c, "账号不存在") + return + } + + utils.Success(c, "密码重置成功", nil) +} diff --git a/server/handlers/system.go b/server/handlers/system.go new file mode 100644 index 0000000..a7f142b --- /dev/null +++ b/server/handlers/system.go @@ -0,0 +1,112 @@ +package handlers + +import ( + "cursorpool-server/database" + "cursorpool-server/models" + "cursorpool-server/utils" + "os" + "runtime" + + "github.com/gin-gonic/gin" +) + +// GetVersion 获取版本信息 +// GET /version +// 用于客户端检查更新(对应 tauri updater 的 latest.json) +func GetVersion(c *gin.Context) { + utils.Success(c, "获取成功", gin.H{ + "version": "1.0.0", + "note": "Cursor Pool 后端服务", + "download_url": "", + }) +} + +// GetPublicInfo 获取公告信息 +// GET /public/info +// 对齐客户端 endpoints.rs get_public_info +// 返回 PublicInfo (types.rs:159-180) +func GetPublicInfo(c *gin.Context) { + var pub models.PublicInfo + if err := database.DB.First(&pub).Error; err != nil { + // 没有公告时返回空 + utils.Success(c, "暂无公告", gin.H{ + "type": "info", + "closeable": true, + "props": gin.H{ + "title": "", + "description": "", + }, + "actions": []interface{}{}, + }) + return + } + + utils.Success(c, "获取成功", gin.H{ + "type": pub.Type, + "closeable": pub.Closeable, + "props": gin.H{ + "title": pub.Title, + "description": pub.Description, + }, + "actions": []gin.H{ + { + "type": pub.ActionType, + "text": pub.ActionText, + "url": pub.ActionURL, + }, + }, + }) +} + +// GetArticleList 获取公告列表 +// GET /article/list/:page +// 对齐客户端 endpoints.rs get_article_list / fetch_article_list +// 客户端固定请求 /article/list/1,返回 data 为 Article 数组 (types.rs:310-316) +func GetArticleList(c *gin.Context) { + page := c.Param("page") + _ = page // 客户端固定传 1,这里简单返回全部 + + var articles []models.Article + database.DB.Order("id DESC").Find(&articles) + + // 组装为客户端期望的格式 {id, title, content} + list := make([]gin.H, 0, len(articles)) + for _, a := range articles { + list = append(list, gin.H{ + "id": a.ID, + "title": a.Title, + "content": a.Content, + }) + } + + utils.Success(c, "获取公告成功", list) +} + +// ReportBug 报告错误 +// POST /report json: BugReportRequest (types.rs:243-264) +// 对齐客户端 endpoints.rs report_bug +func ReportBug(c *gin.Context) { + var req struct { + APIKey *[]string `json:"api_key,omitempty"` + AppVersion string `json:"app_version"` + OSVersion string `json:"os_version"` + DeviceModel string `json:"device_model"` + CursorVersion string `json:"cursor_version"` + BugDescription string `json:"bug_description"` + OccurrenceTime string `json:"occurrence_time"` + ScreenshotURLs *[]string `json:"screenshot_urls,omitempty"` + Severity string `json:"severity"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.Fail(c, "参数错误: "+err.Error()) + return + } + + // TODO: 存库或转发到错误收集系统 + hostname, _ := os.Hostname() + _ = runtime.GOOS + println("[BUG REPORT]", hostname, req.BugDescription, req.Severity) + + utils.Success(c, "报告成功", nil) +} diff --git a/server/handlers/user.go b/server/handlers/user.go new file mode 100644 index 0000000..79e33fb --- /dev/null +++ b/server/handlers/user.go @@ -0,0 +1,124 @@ +package handlers + +import ( + "time" + + "cursorpool-server/database" + "cursorpool-server/models" + "cursorpool-server/utils" + + "github.com/gin-gonic/gin" + "golang.org/x/crypto/bcrypt" +) + +// GetUserInfo 获取用户信息 +// GET /user (需 JWT) +// 对齐客户端 endpoints.rs get_user_info +// 返回 UserInfo (types.rs:32-59) +func GetUserInfo(c *gin.Context) { + userID := c.GetInt("user_id") + + var user models.User + if err := database.DB.First(&user, userID).Error; err != nil { + utils.Fail(c, "用户不存在") + return + } + + utils.Success(c, "获取成功", gin.H{ + "totalCount": user.TotalCount, + "usedCount": user.UsedCount, + "expireTime": user.ExpireTime.Format("2006-01-02 15:04:05"), + "level": user.Level, + "isExpired": user.ExpireTime.Before(time.Now()), + "username": user.Username, + "code_level": user.CodeLevel, + "code_status": user.CodeStatus, + }) +} + +// Activate 激活账户 +// POST /user/activate form: code (需 JWT) +// 对齐客户端 endpoints.rs activate +func Activate(c *gin.Context) { + userID := c.GetInt("user_id") + code := c.PostForm("code") + if code == "" { + utils.Fail(c, "激活码不能为空") + return + } + + // 查激活码 + var ac models.ActivationCode + if err := database.DB.Where("code = ? AND status = 1", code).First(&ac).Error; err != nil { + utils.Fail(c, "激活码无效") + return + } + + // 检查使用次数 + if ac.UsedCount >= ac.MaxUses { + utils.Fail(c, "激活码已达使用上限") + return + } + + // 更新用户:提升等级 + 延长有效期 + 增加额度 + expire := time.Now() + if expire.Before(time.Now()) { + expire = time.Now() + } + expire = expire.Add(time.Duration(ac.Duration) * 24 * time.Hour) + + result := database.DB.Model(&models.User{}).Where("id = ?", userID).Updates(map[string]interface{}{ + "level": ac.Level, + "expire_time": expire, + "total_count": 1000, // TODO: 按业务设置额度 + "code_status": 1, + }) + if result.RowsAffected == 0 { + utils.Fail(c, "激活失败") + return + } + + // 激活码使用次数 +1 + database.DB.Model(&ac).UpdateColumn("used_count", ac.UsedCount+1) + + utils.Success(c, "激活成功", gin.H{ + "expireTime": expire.Unix(), + "level": ac.Level, + }) +} + +// UpdatePassword 修改密码 +// POST /user/updatePassword form: old_password, new_password, confirm_password (需 JWT) +// 对齐客户端 endpoints.rs change_password +// 注意:修改成功后客户端会清除 token,需重新登录(见 client.rs:122-133) +func UpdatePassword(c *gin.Context) { + userID := c.GetInt("user_id") + oldPwd := c.PostForm("old_password") + newPwd := c.PostForm("new_password") + confirmPwd := c.PostForm("confirm_password") + + if oldPwd == "" || newPwd == "" { + utils.Fail(c, "参数不完整") + return + } + if newPwd != confirmPwd { + utils.Fail(c, "两次密码不一致") + return + } + + var user models.User + if err := database.DB.First(&user, userID).Error; err != nil { + utils.Fail(c, "用户不存在") + return + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPwd)); err != nil { + utils.Fail(c, "原密码错误") + return + } + + hash, _ := bcrypt.GenerateFromPassword([]byte(newPwd), bcrypt.DefaultCost) + database.DB.Model(&user).Update("password", string(hash)) + + utils.Success(c, "密码修改成功", nil) +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..d730d76 --- /dev/null +++ b/server/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "log" + + "cursorpool-server/config" + "cursorpool-server/database" + "cursorpool-server/routes" + + "github.com/gin-gonic/gin" +) + +func main() { + // 加载配置 + config.Load() + + // 初始化数据库(自动建表 + 种子数据) + database.Init() + + // 启动 Gin + gin.SetMode(gin.ReleaseMode) + r := gin.Default() + + // 注册路由 + routes.Setup(r) + + addr := ":" + config.App.Port + fmt.Printf("Cursor Pool 后端服务已启动\n") + fmt.Printf("监听地址: http://localhost%s\n", addr) + fmt.Printf("默认账号: demo@cursorpool.test\n") + fmt.Printf("默认密码: 123456\n") + fmt.Printf("数据库路径: %s\n", config.App.DBPath) + fmt.Println("-------------------------------------------") + + if err := r.Run(addr); err != nil { + log.Fatalf("服务启动失败: %v", err) + } +} diff --git a/server/middleware/middleware.go b/server/middleware/middleware.go new file mode 100644 index 0000000..1ae9e69 --- /dev/null +++ b/server/middleware/middleware.go @@ -0,0 +1,86 @@ +package middleware + +import ( + "net/http" + "strings" + "time" + + "cursorpool-server/config" + "cursorpool-server/utils" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +// JWTClaims JWT 载荷 +type JWTClaims struct { + UserID int `json:"user_id"` + Email string `json:"email"` + jwt.RegisteredClaims +} + +// GenerateToken 生成 JWT +func GenerateToken(userID int, email string) (string, error) { + claims := JWTClaims{ + UserID: userID, + Email: email, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(365 * 24 * time.Hour)), // 365天,按需调整 + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(config.App.JWTSecret)) +} + +// ParseToken 解析 JWT +func ParseToken(tokenStr string) (*JWTClaims, error) { + claims := &JWTClaims{} + _, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) { + return []byte(config.App.JWTSecret), nil + }) + if err != nil { + return nil, err + } + return claims, nil +} + +// JWTAuth JWT 鉴权中间件 +// 客户端通过 Authorization: Bearer 传递(见 interceptor.rs:37-40) +func JWTAuth() gin.HandlerFunc { + return func(c *gin.Context) { + auth := c.GetHeader("Authorization") + if auth == "" || !strings.HasPrefix(auth, "Bearer ") { + utils.FailWithCode(c, "未登录或登录已过期", "UNAUTHORIZED") + c.Abort() + return + } + + tokenStr := strings.TrimPrefix(auth, "Bearer ") + claims, err := ParseToken(tokenStr) + if err != nil { + utils.FailWithCode(c, "登录凭证无效", "UNAUTHORIZED") + c.Abort() + return + } + + c.Set("user_id", claims.UserID) + c.Set("email", claims.Email) + c.Next() + } +} + +// CORS 跨域中间件 +func CORS() gin.HandlerFunc { + return func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, cb-lang") + c.Header("Access-Control-Max-Age", "86400") + if c.Request.Method == http.MethodOptions { + c.AbortWithStatus(http.StatusNoContent) + return + } + c.Next() + } +} diff --git a/server/models/models.go b/server/models/models.go new file mode 100644 index 0000000..e3bdab6 --- /dev/null +++ b/server/models/models.go @@ -0,0 +1,77 @@ +package models + +import ( + "time" +) + +// User 注册用户(对齐客户端 types.rs UserInfo) +type User struct { + ID int `gorm:"primaryKey" json:"id"` + Email string `gorm:"uniqueIndex;size:128" json:"email"` + Password string `gorm:"size:128" json:"-"` // bcrypt hash,不输出 + Username string `gorm:"size:64" json:"username"` + Level int `gorm:"default:0" json:"level"` + CodeLevel string `gorm:"size:32" json:"code_level"` + CodeStatus int `gorm:"default:0" json:"code_status"` + TotalCount int `gorm:"default:0" json:"totalCount"` // 总额度 + UsedCount int `gorm:"default:0" json:"usedCount"` // 已用额度 + ExpireTime time.Time `json:"expireTime"` // 过期时间 + IsExpired bool `gorm:"default:false" json:"isExpired"` + Spread string `gorm:"size:32;default:0" json:"-"` // 邀请码 + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// CursorAccount Cursor 账户池(对齐客户端 types.rs AccountInfo) +type CursorAccount struct { + ID int `gorm:"primaryKey" json:"id"` + Email string `gorm:"size:128" json:"account"` + Password string `gorm:"size:128" json:"password"` + Token string `gorm:"size:512" json:"token"` + UsageCount int `gorm:"default:0" json:"usage_count"` + Status int `gorm:"default:1" json:"status"` // 1=可用 0=禁用 + CreateTime time.Time `json:"create_time"` + DistributedTime time.Time `json:"distributed_time"` + UpdateTime time.Time `json:"update_time"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` +} + +// ActivationCode 激活码(对齐客户端 types.rs ActivationCode) +type ActivationCode struct { + ID int `gorm:"primaryKey" json:"id"` + Code string `gorm:"uniqueIndex;size:64" json:"code"` + Type int `json:"type"` + Name string `gorm:"size:64" json:"name"` + Level int `json:"level"` + Duration int `json:"duration"` // 天数 + MaxUses int `json:"max_uses"` + UsedCount int `json:"used_count"` + Status int `gorm:"default:1" json:"status"` + Notes string `json:"notes"` + ActivatedAt time.Time `json:"activated_at"` + ExpiredAt time.Time `json:"expired_at"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` +} + +// Article 公告(对齐客户端 types.rs Article) +type Article struct { + ID int `gorm:"primaryKey" json:"id"` + Title string `gorm:"size:128" json:"title"` + Content string `gorm:"type:text" json:"content"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` +} + +// PublicInfo 公告信息(对齐客户端 types.rs PublicInfo) +type PublicInfo struct { + ID int `gorm:"primaryKey" json:"id"` + Type string `gorm:"size:32" json:"type"` // info/warning/error + Closeable bool `json:"closeable"` + Title string `gorm:"size:128" json:"title"` + Description string `gorm:"type:text" json:"description"` + ActionText string `gorm:"size:64" json:"action_text"` + ActionURL string `gorm:"size:256" json:"action_url"` + ActionType string `gorm:"size:32" json:"action_type"` // link/button +} diff --git a/server/routes/routes.go b/server/routes/routes.go new file mode 100644 index 0000000..4d5d396 --- /dev/null +++ b/server/routes/routes.go @@ -0,0 +1,40 @@ +package routes + +import ( + "cursorpool-server/handlers" + "cursorpool-server/middleware" + + "github.com/gin-gonic/gin" +) + +// Setup 注册所有路由 +// 对齐 swagger.json 定义的 14 个接口 +func Setup(r *gin.Engine) { + r.Use(middleware.CORS()) + + // 公开接口(无需 JWT) + pub := r.Group("") + { + pub.POST("/checkUser", handlers.CheckUser) + pub.POST("/register/sendEmailCode", handlers.SendCode) + pub.POST("/emailRegister", handlers.Register) + pub.POST("/emailResetPassword", handlers.ResetPassword) + pub.POST("/login", handlers.Login) + + pub.GET("/public/info", handlers.GetPublicInfo) + pub.GET("/version", handlers.GetVersion) + pub.GET("/article/list/:page", handlers.GetArticleList) + pub.POST("/report", handlers.ReportBug) + } + + // 需鉴权接口(需 JWT,Authorization: Bearer ) + auth := r.Group("") + auth.Use(middleware.JWTAuth()) + { + auth.GET("/user", handlers.GetUserInfo) + auth.POST("/user/activate", handlers.Activate) + auth.POST("/user/updatePassword", handlers.UpdatePassword) + + auth.GET("/accountpool/get", handlers.GetAccount) + } +} diff --git a/server/utils/response.go b/server/utils/response.go new file mode 100644 index 0000000..148bff2 --- /dev/null +++ b/server/utils/response.go @@ -0,0 +1,45 @@ +package utils + +import "github.com/gin-gonic/gin" + +// ApiResponse 统一响应结构(对齐客户端 types.rs ApiResponse) +type ApiResponse struct { + Status int `json:"status"` + Msg string `json:"msg"` + Data interface{} `json:"data,omitempty"` + Code string `json:"code,omitempty"` +} + +const ( + StatusOK = 200 + StatusFail = 500 + StatusAuth = 401 + CodeSuccess = "SUCCESS" +) + +// Success 成功响应 +func Success(c *gin.Context, msg string, data interface{}) { + c.JSON(200, ApiResponse{ + Status: StatusOK, + Msg: msg, + Data: data, + Code: CodeSuccess, + }) +} + +// Fail 失败响应 +func Fail(c *gin.Context, msg string) { + c.JSON(200, ApiResponse{ + Status: StatusFail, + Msg: msg, + }) +} + +// FailWithCode 失败响应(带业务 code) +func FailWithCode(c *gin.Context, msg string, code string) { + c.JSON(200, ApiResponse{ + Status: StatusFail, + Msg: msg, + Code: code, + }) +}