diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 1a7d411..5105209 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -85,13 +85,14 @@ 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 uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'pnpm' - name: install Rust stable @@ -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 78ae5f9..5405740 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: @@ -34,17 +38,20 @@ jobs: name: Build ${{ matrix.display_name }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - 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 uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'pnpm' - name: Get Version (Windows) @@ -63,6 +70,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: @@ -80,9 +99,11 @@ jobs: - name: Install dependencies run: pnpm install + env: + HUSKY: '0' - 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 +128,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: @@ -125,17 +146,20 @@ jobs: name: Build ${{ matrix.display_name }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - 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 uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'pnpm' - name: Get Version @@ -145,6 +169,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: @@ -157,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" + ] } } 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, + }) +} 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",