diff --git a/.claude/plan/sync-features.md b/.claude/plan/sync-features.md
deleted file mode 100644
index 580b9ab..0000000
--- a/.claude/plan/sync-features.md
+++ /dev/null
@@ -1,241 +0,0 @@
-# 功能同步计划:私有版 → 开源版
-
-源目录:`/Users/Apple/vpn/ppanel-server`(私有版)
-目标目录:`/Users/Apple/code_vpn/vpn/ppanel-server`(开源版)
-模块名相同:`github.com/perfect-panel/server`
-
----
-
-## 批次 A:新增独立包(直接复制)
-
-### A1. pkg/iap(Apple IAP)
-- 复制 `pkg/iap/` 全目录
-
-### A2. pkg/kutt(短链接服务)
-- 复制 `pkg/kutt/kutt.go`
-
-### A3. pkg/loki(Grafana Loki)
-- 复制 `pkg/loki/loki.go`
-
-### A4. pkg/openinstall(渠道统计)
-- 复制 `pkg/openinstall/openinstall.go`
-
-### A5. internal/model/iap(IAP 数据模型)
-- 复制 `internal/model/iap/` 全目录
-
-### A6. internal/model/logmessage(错误日志模型)
-- 复制 `internal/model/logmessage/` 全目录
-
----
-
-## 批次 B:数据库迁移
-
-### B1. 新增迁移文件(仅从私有版复制)
-- `02120_log_message.up.sql` / `.down.sql`
-- `02121_apple_iap_transactions.up.sql` / `.down.sql`
-- `02122_add_user_last_login_time.up.sql` / `.down.sql`
-- `02123_update_auth_method_config.up.sql` / `.down.sql`(auth_method MEDIUMTEXT)
-
-### B2. 修改共有迁移文件
-- `00002_init_basic_data.up.sql`:VerifyCodeExpireTime 默认值 300→900
-- `02118_traffic_log_idx.up.sql`:添加幂等性检查
-- `02119_node.up.sql`:@sql 变量名修正(已是私有版逻辑,保留)
-
----
-
-## 批次 C:配置与基础设施
-
-### C1. internal/config/config.go
-- 添加:KuttConfig、LokiConfig、OpenInstallConfig、AppleIAPConfig 结构体
-- 添加:FixedRate 浮点型汇率配置
-
-### C2. internal/config/cacheKey.go
-- 添加:`UserSessionsKeyPrefix = "auth:user_sessions:"`
-
-### C3. pkg/conf/default.go
-- 添加:reflect.Float64 case 处理
-
-### C4. internal/svc/serviceContext.go
-- 添加:Kutt、Loki、OpenInstall、IAPModel 字段
-
-### C5. internal/svc/devce.go
-- 修复 SQL Bug:`create_at` → `created_at`
-
-### C6. pkg/orm/mysql.go
-- 添加:SetConnMaxIdleTime(5min) + SetConnMaxLifetime(30min)
-
-### C7. pkg/exchangeRate/exchangeRate.go
-- 保持私有版的 exchangerate.host 实现
-
-### C8. initialize/config.go & init.go
-- 添加 Loki、OpenInstall 初始化逻辑
-
----
-
-## 批次 D:数据模型
-
-### D1. internal/model/user/user.go
-- 添加:LastLoginTime 字段
-
-### D2. internal/model/auth/auth.go
-- config 字段 MEDIUMTEXT(Go 结构体 tag)
-- 补全 4 种邮件模板初始化
-
-### D3. internal/model/node/model.go
-- 添加:CountNodesByIdsAndTags 方法(移除 fmt.Println 调试语句)
-- 修复:ClearServerAllCache 同时清理 ServerConfig + ServerUserList
-- 修复:cursor append bug(OSS版的 append(keys, keys...))
-
-### D4. internal/model/payment/payment.go
-- 添加:AppleIAPConfig 结构体
-
-### D5. internal/model/user/subscribe.go
-- 添加:includeExpired 参数支持(":all" cache key suffix)
-
----
-
-## 批次 E:认证与登录逻辑
-
-### E1. internal/logic/auth/userLoginLogic.go
-- 添加:LastLoginTime 更新
-- 添加:邮箱登录调试日志清理
-
-### E2. internal/logic/auth/userRegisterLogic.go
-- 添加:邮箱小写/trim 处理
-- 添加:验证码过期时间使用配置值
-
-### E3. 新增:internal/logic/auth/emailLoginLogic.go
-- 从私有版复制邮箱直登逻辑
-
-### E4. 新增:apis/auth/auth.api 邮箱登录路由
-- 添加 emailLogin 接口定义
-
----
-
-## 批次 F:用户功能逻辑
-
-### F1. internal/logic/public/user/queryUserInfoLogic.go
-- 添加:Kutt 短链接生成 + Redis 永久缓存
-- 添加:share_link 字段返回
-
-### F2. internal/logic/public/user/queryUserSubscribeLogic.go
-- 添加:查询 Order 判断 IsGift(amount==0)
-
-### F3. internal/handler/public/user/queryUserSubscribeHandler.go
-- 添加:includeExpired query param 注入 context
-
-### F4. internal/model/user/model.go
-- 添加:FindActiveSubscribe / FindActiveSubscribesByUserIds
-- 添加:BatchClearRelatedCache
-- 添加:DeleteUserAuthMethodByIdentifier
-
----
-
-## 批次 G:订单与支付
-
-### G1. internal/logic/public/order/preCreateOrderLogic.go
-- 修复:math.Round 金额计算
-
-### G2. internal/logic/public/order/purchaseLogic.go
-- 修复:math.Round
-
-### G3. internal/logic/public/order/renewalLogic.go
-- 修复:math.Round
-
-### G4. internal/logic/public/portal/purchaseCheckoutLogic.go
-- 添加:Apple IAP checkout case
-- 添加:currency 从 DB 动态读取 + FixedRate fallback
-- 修复:math.Round for alipay
-
-### G5. internal/model/payment/model.go
-- 添加:AppleIAP payment model 支持
-
-### G6. pkg/payment/platform.go
-- 添加:AppleIAP platform entry
-
----
-
-## 批次 H:订阅与节点
-
-### H1. internal/logic/public/portal/getSubscriptionLogic.go
-- 添加:CountNodesByIdsAndTags 调用,返回 node_count
-
-### H2. internal/logic/public/portal/purchaseLogic.go
-- 添加:CloseOrderTimeMinutes 超时配置
-
-### H3. internal/logic/subscribe/subscribeLogic.go
-- 保持:PanDomain 模式返回当前 Host
-
----
-
-## 批次 I:队列任务
-
-### I1. queue/logic/order/activateOrderLogic.go
-- 添加:findGiftSubscription 逻辑
-- 添加:extendGiftSubscription 逻辑
-- 添加:grantGiftDaysToBothParties(双向赠礼)
-- 添加:no-retry for invalid status
-
-### I2. queue/logic/email/sendEmailLogic.go
-- 修改:主题中文化
-- 修改:动态过期分钟数
-- 修改:float64→int 类型转换
-- 添加:Smart Fallback 模板逻辑(4种类型)
-
-### I3. queue/logic/task/rateLogic.go
-- 添加:FixedRate config 支持(跳过 API 调用)
-
----
-
-## 批次 J:路由与类型
-
-### J1. internal/handler/routes.go
-- 添加:IAP Apple 路由
-- 添加:email login 路由
-- 添加:error log 路由
-- 添加:contact 路由
-
-### J2. internal/types/types.go
-- 添加:Apple IAP 相关 Request/Response 类型
-- 添加:BindEmailWithVerificationRequest/Response
-- 添加:BindInviteCodeRequest
-- 添加:ProductIds in checkout
-
-### J3. internal/types/subscribe.go
-- 保持私有版(移除 Type/Params 开源版新增字段——已在批次 A 处理)
-
----
-
-## 批次 K:邮件系统
-
-### K1. pkg/email/template.go
-- 添加:Maintenance、TrafficExceed、Verify 3 种邮件模板
-
----
-
-## 批次 L:管理后台
-
-### L1. internal/logic/admin/user/getUserListLogic.go
-- 添加:MemberStatus + LastLoginTime 展示
-- 添加:批量获取 active subscriptions
-
-### L2. internal/logic/admin/user/updateUserBasicInfoLogic.go
-- 添加:Remark + MemberStatus 更新
-- 添加:单事务包裹所有修改
-
----
-
-## 执行顺序
-
-1. A(新包) → B(迁移) → C(配置) → D(模型)
-2. E(认证) → F(用户) → G(订单) → H(订阅)
-3. I(队列) → J(路由) → K(邮件) → L(管理后台)
-
----
-
-## 注意事项
-
-- **排除**:App版本管理、设备绑定相关逻辑(ZSet Session、DeviceId in token)
-- **调试日志清理**:不带 fmt.Println 调试语句
-- **模块名相同**:import 路径无需修改
-- **开源版新功能保留**:软删除、注册 IP 限流、兑换码系统、GeoIP 等开源版独有功能不回退
diff --git a/.claude/team-plan/sync-remaining.md b/.claude/team-plan/sync-remaining.md
deleted file mode 100644
index 7e5dbce..0000000
--- a/.claude/team-plan/sync-remaining.md
+++ /dev/null
@@ -1,168 +0,0 @@
-# Team Plan: 同步私有版剩余功能到 OSS
-
-源目录:`/Users/Apple/vpn/ppanel-server`(私有版)
-目标目录:`/Users/Apple/code_vpn/vpn/ppanel-server`(开源版)
-方向:私有版功能 → 开源版(保留开源版改进)
-
----
-
-## Layer 1(并行)
-
-### Builder-1: 路由与服务上下文(高复杂度)
-
-**文件范围:**
-- `internal/handler/routes.go`
-- `internal/svc/serviceContext.go`
-
-**任务:routes.go**
-1. 读取私有版 routes.go,识别以下需要添加的路由组:
- - Apple IAP 路由组:`/v1/public/iap/apple`(status, attach, restore)
- - Email login 路由:`POST /auth/login/email`
- - Error log report 路由:`POST /common/log/message/report`
- - Contact 路由:`POST /common/contact`
- - Bind email with verification:`POST /public/user/bind_email_verification`
- - Bind invite code:`POST /public/user/bind_invite_code`
- - Subscribe status:`GET /public/user/subscribe/status`
- - Agent stats/downloads/sales 路由
- - Delete account:`POST /public/user/delete_account`
- - Admin error log routes:`GET /admin/log/message/error/list`, `/detail`
-2. 保留 OSS 已有路由(Redemption、WS、heartbeat、reset_token、reset_traffic、toggle_status、IP location、module config 等)
-3. 不添加 App Version 路由(`/admin/application/version`、`/common/app/version`)
-4. 不添加 Server migration 路由
-
-**任务:serviceContext.go**
-1. 添加 import:`logmessage`、`iapapple` model 包
-2. 在 ServiceContext struct 中添加字段:
- - `LogMessageModel logmessage.Model`
- - `IAPAppleTransactionModel iapapple.Model`
-3. 在 NewServiceContext 中初始化这些字段
-4. 保留 OSS 已有字段(GeoIP、RedemptionCodeModel、RedemptionRecordModel、Redis 扩展配置)
-5. 不添加 SessionLimit() 和 EnforceUserSessionLimit() 方法(设备绑定相关)
-
-**验收标准:**
-- `go build ./...` 编译通过
-- 所有新路由正确注册
-- 不包含设备绑定和 App 版本管理路由
-
----
-
-### Builder-2: 认证与注册逻辑(中复杂度)
-
-**文件范围:**
-- `internal/logic/auth/userRegisterLogic.go`
-- `internal/logic/common/sendEmailCodeLogic.go`
-- `internal/middleware/deviceMiddleware.go`
-
-**任务:userRegisterLogic.go**
-1. 添加邮箱规范化:`req.Email = strings.ToLower(strings.TrimSpace(req.Email))`(在函数开头)
-2. 保留 OSS 已有改进:IP 限流检查、已删除用户检查、activeTrial 返回 Subscribe 对象、事务外清缓存
-3. 不添加 bypass code(`"202511"`)
-4. 不添加 DeviceBindLimitExceeded 错误处理
-
-**任务:sendEmailCodeLogic.go**
-1. 添加邮箱规范化:`req.Email = strings.ToLower(strings.TrimSpace(req.Email))`
-2. 添加动态验证码过期时间:从 `l.svcCtx.Config.VerifyCode.ExpireTime` 读取,默认 900 秒,转换为分钟
-3. Redis TTL 使用配置的过期时间而非硬编码
-4. 保留 OSS 已有的 Security type 手机绑定检查
-5. 不添加 DeleteAccount 邮件类型
-
-**任务:deviceMiddleware.go**
-1. 统一常量名:`constant.LoginType` → `constant.CtxLoginType`
-2. 移除多余 debug 日志
-
-**验收标准:**
-- 邮箱注册/验证码发送时自动 trim 和 lowercase
-- 验证码过期时间可配置
-- 编译通过
-
----
-
-### Builder-3: 管理后台逻辑(中复杂度)
-
-**文件范围:**
-- `internal/logic/admin/user/getUserListLogic.go`
-- `internal/logic/admin/user/updateUserBasicInfoLogic.go`
-- `internal/logic/admin/payment/createPaymentMethodLogic.go`
-
-**任务:getUserListLogic.go**
-1. 读取私有版,添加批量获取活跃订阅逻辑(FindActiveSubscribesByUserIds)
-2. 添加 MemberStatus 计算(基于订阅到期时间判断)
-3. 添加 LastLoginTime 展示
-4. 保留 OSS 的 Unscoped 和 ShortCode 查询参数
-5. 不添加 DeviceId 过滤
-
-**任务:updateUserBasicInfoLogic.go**
-1. 读取私有版,添加 Remark 字段更新支持
-2. 添加 MemberStatus 更新支持(如果有)
-3. 保留 OSS 的错误处理模式(非 fmt.Sprintf 包装)
-4. 考虑使用事务包裹所有修改(参考私有版 Transaction 用法)
-
-**任务:createPaymentMethodLogic.go**
-1. 对比两版差异,同步私有版中有用的改动
-
-**验收标准:**
-- 管理员用户列表显示 MemberStatus 和 LastLoginTime
-- 编译通过
-
----
-
-### Builder-4: 服务器、Handler 和模型修复(低-中复杂度)
-
-**文件范围:**
-- `internal/server.go`
-- `internal/handler/notify.go`
-- `internal/handler/subscribe.go`
-- `internal/logic/server/constant.go`
-- `internal/logic/subscribe/subscribeLogic.go`
-- `initialize/config.go`
-- `initialize/init.go`
-- `cmd/run.go`
-- `internal/model/subscribe/subscribe.go`
-- `internal/model/order/model.go`
-- `internal/model/announcement/model.go`
-- `internal/model/log/log.go`
-- `internal/model/user/model.go`
-- `internal/types/subscribe.go`
-- `pkg/constant/version.go`
-- `internal/svc/devce.go`
-- `internal/logic/server/serverPushUserTrafficLogic.go`
-- `queue/logic/traffic/trafficStatisticsLogic.go`
-- `internal/logic/public/subscribe/queryUserSubscribeNodeListLogic.go`
-- `internal/logic/public/user/resetUserSubscribeTokenLogic.go`
-
-**任务说明:**
-以上大部分文件 OSS 版本已经是正确/更好的版本。Builder 需要:
-1. 逐一 diff 对比,确认 OSS 版本是否完整
-2. 对于已经正确的文件,跳过不做修改
-3. 对于需要微调的文件(如 handler/notify.go 的路由路径斜杠修复),进行小修改
-4. server.go:确认 OSS 有 gateway mode,保持不变
-5. initialize/config.go、init.go:确认 OSS 有 gateway mode + Currency 调用,保持不变
-6. cmd/run.go:确认 trace/init 已移至 server.go,保持不变
-7. handler/notify.go:修复路由路径 `:platform/:token` → `/:platform/:token`(如果 OSS 已修复则跳过)
-8. svc/devce.go:确认 `create_at` → `created_at` bug fix 已应用
-
-**验收标准:**
-- 所有文件状态正确
-- 编译通过
-- 不引入回退(不把 OSS 改进覆盖回去)
-
----
-
-## 排除项(不同步)
-
-- App 版本管理路由和逻辑
-- 设备绑定相关:SessionLimit、EnforceUserSessionLimit、DeviceId context、DeviceBindLimitExceeded
-- adapter/adapter.go、adapter/client.go(保留 OSS 的 Type/Params 改进)
-- internal/model/user/default.go(保留 OSS 的软删除改进)
-- pkg/payment/stripe/stripe.go(保留 OSS 的 ConstructEventWithOptions 改进)
-- getDeviceListLogic.go、unbindDeviceLogic.go(设备绑定)
-- authMethod.go(设备绑定 DeleteUserAuthMethodByIdentifier)
-- device.go model(设备绑定增强)
-- constant/types.go(DeleteAccount 枚举 - 设备绑定)
-- device/device.go(GetOnlineDeviceLoginTime - 设备绑定)
-
----
-
-## 依赖关系
-
-Layer 1 所有 Builder 可并行执行,无互相依赖。
diff --git a/.github/environments/production.yml b/.github/environments/production.yml
deleted file mode 100644
index 8e06538..0000000
--- a/.github/environments/production.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-# Production Environment Configuration for GitHub Actions
-# This file defines production-specific deployment settings
-
-environment:
- name: production
- url: https://api.ppanel.example.com
- protection_rules:
- - type: wait_timer
- minutes: 5
- - type: reviewers
- reviewers:
- - "@admin-team"
- - "@devops-team"
- variables:
- ENVIRONMENT: production
- LOG_LEVEL: info
- DEPLOY_TIMEOUT: 300
-
-# Environment-specific secrets required:
-# PRODUCTION_HOST - Production server hostname/IP
-# PRODUCTION_USER - SSH username for production server
-# PRODUCTION_SSH_KEY - SSH private key for production server
-# PRODUCTION_PORT - SSH port (default: 22)
-# PRODUCTION_URL - Application URL for health checks
-# DATABASE_PASSWORD - Production database password
-# REDIS_PASSWORD - Production Redis password
-# JWT_SECRET - JWT secret key for production
\ No newline at end of file
diff --git a/.github/environments/staging.yml b/.github/environments/staging.yml
deleted file mode 100644
index a62b4c4..0000000
--- a/.github/environments/staging.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-# Staging Environment Configuration for GitHub Actions
-# This file defines staging-specific deployment settings
-
-environment:
- name: staging
- url: https://staging-api.ppanel.example.com
- protection_rules:
- - type: wait_timer
- minutes: 2
- variables:
- ENVIRONMENT: staging
- LOG_LEVEL: debug
- DEPLOY_TIMEOUT: 180
-
-# Environment-specific secrets required:
-# STAGING_HOST - Staging server hostname/IP
-# STAGING_USER - SSH username for staging server
-# STAGING_SSH_KEY - SSH private key for staging server
-# STAGING_PORT - SSH port (default: 22)
-# STAGING_URL - Application URL for health checks
-# DATABASE_PASSWORD - Staging database password
-# REDIS_PASSWORD - Staging Redis password
-# JWT_SECRET - JWT secret key for staging
\ No newline at end of file
diff --git a/.github/workflows/deploy-linux.yml b/.github/workflows/deploy-linux.yml
deleted file mode 100644
index 1996507..0000000
--- a/.github/workflows/deploy-linux.yml
+++ /dev/null
@@ -1,79 +0,0 @@
-name: Build Linux Binary
-
-on:
- push:
- branches: [ main, master ]
- tags:
- - 'v*'
- workflow_dispatch:
- inputs:
- version:
- description: 'Version to build (leave empty for auto)'
- required: false
- type: string
-
-permissions:
- contents: write
-
-jobs:
- build:
- name: Build Linux Binary
- runs-on: ubuntu-latest
-
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Go
- uses: actions/setup-go@v5
- with:
- go-version: '1.23.3'
- cache: true
-
- - name: Build
- env:
- CGO_ENABLED: 0
- GOOS: linux
- GOARCH: amd64
- run: |
- VERSION=${{ github.event.inputs.version }}
- if [ -z "$VERSION" ]; then
- VERSION=$(git describe --tags --always --dirty)
- fi
-
- echo "Building ppanel-server $VERSION"
- BUILD_TIME=$(date +"%Y-%m-%d_%H:%M:%S")
- go build -ldflags="-w -s -X github.com/perfect-panel/server/pkg/constant.Version=$VERSION -X github.com/perfect-panel/server/pkg/constant.BuildTime=$BUILD_TIME" -o ppanel-server ./ppanel.go
- tar -czf ppanel-server-${VERSION}-linux-amd64.tar.gz ppanel-server
- sha256sum ppanel-server ppanel-server-${VERSION}-linux-amd64.tar.gz > checksum.txt
-
- - name: Upload artifacts
- uses: actions/upload-artifact@v4
- with:
- name: ppanel-server-linux-amd64
- path: |
- ppanel-server
- ppanel-server-*-linux-amd64.tar.gz
- checksum.txt
-
- - name: Create Release
- if: startsWith(github.ref, 'refs/tags/')
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- VERSION=${GITHUB_REF#refs/tags/}
-
- # Check if release exists
- if gh release view $VERSION >/dev/null 2>&1; then
- echo "Release $VERSION already exists, deleting old assets..."
- # Delete existing assets if they exist
- gh release delete-asset $VERSION ppanel-server-${VERSION}-linux-amd64.tar.gz --yes 2>/dev/null || true
- gh release delete-asset $VERSION checksum.txt --yes 2>/dev/null || true
- else
- echo "Creating new release $VERSION..."
- gh release create $VERSION --title "PPanel Server $VERSION" --notes "Release $VERSION"
- fi
-
- # Upload assets (will overwrite if --clobber is supported, otherwise will fail gracefully)
- echo "Uploading assets..."
- gh release upload $VERSION ppanel-server-${VERSION}-linux-amd64.tar.gz checksum.txt --clobber
diff --git a/.gitignore b/.gitignore
index 9252942..219fb06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,8 @@
*.local.yaml
/test/
*.log
+*.sh
+script/*.sh
.DS_Store
*_test_config.go
*.log*
@@ -14,4 +16,7 @@
node_modules
package-lock.json
package.json
-/bin
\ No newline at end of file
+/bin
+.claude
+./github
+./run
\ No newline at end of file
diff --git a/.run/go build github.com_perfect-panel_server.run.xml b/.run/go build github.com_perfect-panel_server.run.xml
deleted file mode 100644
index 608afe7..0000000
--- a/.run/go build github.com_perfect-panel_server.run.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/script/db_query.go b/script/db_query.go
deleted file mode 100644
index 60c880c..0000000
--- a/script/db_query.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "flag"
- "fmt"
- "os"
-
- "github.com/perfect-panel/server/internal/config"
- "github.com/perfect-panel/server/pkg/conf"
- "gorm.io/driver/mysql"
- "gorm.io/gorm"
-)
-
-func main() {
- configPath := flag.String("config", "etc/ppanel.yaml", "config file path")
- query := flag.String("query", "", "sql query")
- flag.Parse()
-
- if *query == "" {
- fmt.Fprintln(os.Stderr, "query is required")
- os.Exit(1)
- }
-
- var cfg config.Config
- conf.MustLoad(*configPath, &cfg)
-
- dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", cfg.MySQL.Username, cfg.MySQL.Password, cfg.MySQL.Addr, cfg.MySQL.Dbname, cfg.MySQL.Config)
- db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
- if err != nil {
- fmt.Fprintf(os.Stderr, "connect db failed: %v\n", err)
- os.Exit(1)
- }
-
- rows, err := db.Raw(*query).Rows()
- if err != nil {
- fmt.Fprintf(os.Stderr, "query failed: %v\n", err)
- os.Exit(1)
- }
- defer rows.Close()
-
- columns, err := rows.Columns()
- if err != nil {
- fmt.Fprintf(os.Stderr, "read columns failed: %v\n", err)
- os.Exit(1)
- }
-
- result := make([]map[string]interface{}, 0)
- for rows.Next() {
- values := make([]interface{}, len(columns))
- pointers := make([]interface{}, len(columns))
- for i := range values {
- pointers[i] = &values[i]
- }
- if err = rows.Scan(pointers...); err != nil {
- fmt.Fprintf(os.Stderr, "scan row failed: %v\n", err)
- os.Exit(1)
- }
-
- rowMap := make(map[string]interface{}, len(columns))
- for i, col := range columns {
- v := values[i]
- if b, ok := v.([]byte); ok {
- rowMap[col] = string(b)
- continue
- }
- rowMap[col] = v
- }
- result = append(result, rowMap)
- }
-
- encoder := json.NewEncoder(os.Stdout)
- if err = encoder.Encode(result); err != nil {
- fmt.Fprintf(os.Stderr, "encode result failed: %v\n", err)
- os.Exit(1)
- }
-}
-
diff --git a/script/family_delete_account_cleanup_check.sh b/script/family_delete_account_cleanup_check.sh
deleted file mode 100755
index 8aaaf56..0000000
--- a/script/family_delete_account_cleanup_check.sh
+++ /dev/null
@@ -1,346 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
-CONFIG_PATH="${CONFIG_PATH:-etc/ppanel.yaml}"
-EMAIL_A="${EMAIL_A:-family_cleanup_$(date +%s)@example.com}"
-DEVICE_A="${DEVICE_A:-qa-cleanup-device-a-$(date +%s)}"
-DEVICE_B="${DEVICE_B:-qa-cleanup-device-b-$(date +%s)}"
-SEND_CODE_RETRY_WAIT="${SEND_CODE_RETRY_WAIT:-61}"
-SEND_CODE_MAX_RETRY="${SEND_CODE_MAX_RETRY:-8}"
-VERIFY_CODE_PROVIDER="${VERIFY_CODE_PROVIDER:-}"
-VERIFY_CODE_OVERRIDE="${VERIFY_CODE_OVERRIDE:-}"
-REGISTER_CODE_OVERRIDE="${REGISTER_CODE_OVERRIDE:-}"
-SECURITY_CODE_OVERRIDE="${SECURITY_CODE_OVERRIDE:-}"
-DELETE_ACCOUNT_CODE_OVERRIDE="${DELETE_ACCOUNT_CODE_OVERRIDE:-}"
-ALLOW_INTERACTIVE_INPUT="${ALLOW_INTERACTIVE_INPUT:-auto}"
-ENABLE_REDIS_CODE_FALLBACK="${ENABLE_REDIS_CODE_FALLBACK:-1}"
-REDIS_CLI_BIN="${REDIS_CLI_BIN:-redis-cli}"
-REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
-REDIS_PORT="${REDIS_PORT:-6379}"
-REDIS_DB="${REDIS_DB:-0}"
-REDIS_PASSWORD="${REDIS_PASSWORD:-}"
-BIND_VERIFY_RETRY="${BIND_VERIFY_RETRY:-1}"
-
-bold() { printf "\033[1m%s\033[0m\n" "$*"; }
-info() { printf "[INFO] %s\n" "$*" >&2; }
-warn() { printf "[WARN] %s\n" "$*" >&2; }
-fail() { printf "[FAIL] %s\n" "$*" >&2; exit 1; }
-
-require_cmd() {
- command -v "$1" >/dev/null 2>&1 || fail "缺少命令: $1"
-}
-
-json_code() { jq -r '.code // ""' <<<"$1"; }
-json_msg() { jq -r '.msg // ""' <<<"$1"; }
-
-can_prompt_input() {
- if [[ "$ALLOW_INTERACTIVE_INPUT" == "1" ]]; then
- return 0
- fi
- if [[ "$ALLOW_INTERACTIVE_INPUT" == "0" ]]; then
- return 1
- fi
- [[ -t 0 ]]
-}
-
-redis_get_key_raw() {
- local key="$1"
- command -v "$REDIS_CLI_BIN" >/dev/null 2>&1 || return 1
-
- local -a args
- args=("$REDIS_CLI_BIN" -h "$REDIS_HOST" -p "$REDIS_PORT" -n "$REDIS_DB" --raw)
- if [[ -n "$REDIS_PASSWORD" ]]; then
- args+=(-a "$REDIS_PASSWORD")
- fi
- "${args[@]}" GET "$key" 2>/dev/null || return 1
-}
-
-get_verify_code_from_redis() {
- local email="$1"
- local typ="$2"
- local scene key raw code
-
- if [[ "$ENABLE_REDIS_CODE_FALLBACK" != "1" ]]; then
- return 1
- fi
-
- case "$typ" in
- 1) scene="register" ;;
- 2) scene="security" ;;
- 4) scene="delete_account" ;;
- *) scene="unknown" ;;
- esac
-
- key="auth:verify:email:${scene}:${email}"
- raw="$(redis_get_key_raw "$key" || true)"
- [[ -n "$raw" ]] || return 1
-
- code="$(jq -r '.code // ""' <<<"$raw" 2>/dev/null || true)"
- [[ -n "$code" && "$code" != "null" ]] || return 1
-
- printf "%s" "$code"
- return 0
-}
-
-api_request() {
- local method="$1"
- local path="$2"
- local body="${3:-}"
- local token="${4:-}"
-
- local -a args
- args=(-sS -X "$method" "${BASE_URL}${path}" -H "Content-Type: application/json")
- if [[ -n "$token" ]]; then
- args+=(-H "Authorization: ${token}")
- fi
- if [[ "$method" != "GET" ]]; then
- args+=(-d "$body")
- fi
- curl "${args[@]}"
-}
-
-assert_success() {
- local resp="$1"
- local step="$2"
- local code
- code="$(json_code "$resp")"
- if [[ "$code" != "200" ]]; then
- fail "${step} 失败, code=${code}, msg=$(json_msg "$resp"), resp=${resp}"
- fi
-}
-
-token_or_keep() {
- local resp="$1"
- local fallback="$2"
- local token
- token="$(jq -r '.data.token // ""' <<<"$resp")"
- if [[ -n "$token" && "$token" != "null" ]]; then
- printf "%s" "$token"
- else
- printf "%s" "$fallback"
- fi
-}
-
-device_login() {
- local identifier="$1"
- local user_agent="$2"
- local payload resp token
-
- payload="$(jq -nc --arg identifier "$identifier" --arg user_agent "$user_agent" '{identifier:$identifier,user_agent:$user_agent}')"
- resp="$(api_request "POST" "/v1/auth/login/device" "$payload")"
- assert_success "$resp" "设备登录(${identifier})"
-
- token="$(jq -r '.data.token // ""' <<<"$resp")"
- [[ -n "$token" && "$token" != "null" ]] || fail "设备登录(${identifier})返回空 token: ${resp}"
- printf "%s" "$token"
-}
-
-send_code_with_retry() {
- local email="$1"
- local typ="$2"
- local payload resp code retry
-
- payload="$(jq -nc --arg email "$email" --argjson type "$typ" '{email:$email,type:$type}')"
- retry=0
- while true; do
- resp="$(api_request "POST" "/v1/common/send_code" "$payload")"
- code="$(json_code "$resp")"
- if [[ "$code" == "200" ]]; then
- printf "%s" "$resp"
- return 0
- fi
- if [[ "$code" == "401" ]]; then
- retry=$((retry + 1))
- if (( retry > SEND_CODE_MAX_RETRY )); then
- fail "发送验证码达到重试上限, email=${email}, type=${typ}, resp=${resp}"
- fi
- warn "验证码发送频率限制,等待 ${SEND_CODE_RETRY_WAIT}s 后重试(${retry}/${SEND_CODE_MAX_RETRY})..."
- sleep "${SEND_CODE_RETRY_WAIT}"
- continue
- fi
- fail "发送验证码失败, email=${email}, type=${typ}, code=${code}, msg=$(json_msg "$resp"), resp=${resp}"
- done
-}
-
-get_verify_code() {
- local email="$1"
- local typ="$2"
- local send_resp code
-
- send_resp="$(send_code_with_retry "$email" "$typ")"
- code="$(jq -r '.data.code // ""' <<<"$send_resp")"
- if [[ -n "$code" && "$code" != "null" ]]; then
- info "验证码已从接口返回(type=${typ})"
- printf "%s" "$code"
- return 0
- fi
-
- code="$(get_verify_code_from_redis "$email" "$typ" || true)"
- if [[ -n "$code" ]]; then
- info "验证码已从 Redis 回读(type=${typ})"
- printf "%s" "$code"
- return 0
- fi
-
- if [[ -n "$VERIFY_CODE_PROVIDER" ]]; then
- code="$("$VERIFY_CODE_PROVIDER" "$email" "$typ" || true)"
- if [[ -n "$code" ]]; then
- info "验证码由 VERIFY_CODE_PROVIDER 提供(type=${typ})"
- printf "%s" "$code"
- return 0
- fi
- fi
-
- if [[ "$typ" == "1" && -n "$REGISTER_CODE_OVERRIDE" ]]; then
- printf "%s" "$REGISTER_CODE_OVERRIDE"
- return 0
- fi
- if [[ "$typ" == "2" && -n "$SECURITY_CODE_OVERRIDE" ]]; then
- printf "%s" "$SECURITY_CODE_OVERRIDE"
- return 0
- fi
- if [[ "$typ" == "4" && -n "$DELETE_ACCOUNT_CODE_OVERRIDE" ]]; then
- printf "%s" "$DELETE_ACCOUNT_CODE_OVERRIDE"
- return 0
- fi
- if [[ -n "$VERIFY_CODE_OVERRIDE" ]]; then
- printf "%s" "$VERIFY_CODE_OVERRIDE"
- return 0
- fi
-
- if ! can_prompt_input; then
- fail "验证码不可自动获取(type=${typ}, email=${email})"
- fi
- read -r -p "请输入邮箱 ${email} 的验证码(type=${typ}):" code
- [[ -n "$code" ]] || fail "验证码为空"
- printf "%s" "$code"
-}
-
-bind_email_with_verification() {
- local token="$1"
- local email="$2"
- local code="$3"
- local payload
- payload="$(jq -nc --arg email "$email" --arg code "$code" '{email:$email,code:$code}')"
- api_request "POST" "/v1/public/user/bind_email_with_verification" "$payload" "$token"
-}
-
-bind_email_with_verification_auto() {
- local token="$1"
- local email="$2"
- local typ="$3"
- local max_retry="${4:-$BIND_VERIFY_RETRY}"
- local attempt=0 code resp api_code
-
- while true; do
- code="$(get_verify_code "$email" "$typ")"
- resp="$(bind_email_with_verification "$token" "$email" "$code")"
- api_code="$(json_code "$resp")"
- if [[ "$api_code" == "200" ]]; then
- printf "%s" "$resp"
- return 0
- fi
- if [[ "$api_code" == "70001" && "$attempt" -lt "$max_retry" ]]; then
- warn "绑定邮箱验证码校验失败,重新获取验证码重试($((attempt+1))/${max_retry})..."
- attempt=$((attempt + 1))
- continue
- fi
- fail "绑定邮箱(${email}) 失败, code=${api_code}, msg=$(json_msg "$resp"), resp=${resp}"
- done
-}
-
-delete_account() {
- local token="$1"
- local email="$2"
- local code="$3"
- local payload resp
- payload="$(jq -nc --arg email "$email" --arg code "$code" '{email:$email,code:$code}')"
- resp="$(api_request "POST" "/v1/public/user/delete_account" "$payload" "$token")"
- assert_success "$resp" "注销账号(${email})"
- printf "%s" "$resp"
-}
-
-db_query_json() {
- local query="$1"
- GOCACHE=/tmp/go-build go run ./script/db_query.go --config "$CONFIG_PATH" --query "$query"
-}
-
-assert_db_field_equals() {
- local json="$1"
- local field="$2"
- local expected="$3"
- local actual
- actual="$(jq -r ".[0].${field} // \"\"" <<<"$json")"
- if [[ "$actual" != "$expected" ]]; then
- fail "DB校验失败: 字段 ${field} 期望=${expected}, 实际=${actual}, 数据=${json}"
- fi
-}
-
-main() {
- require_cmd curl
- require_cmd jq
- require_cmd go
-
- bold "家庭组注销清理校验脚本"
- info "BASE_URL=${BASE_URL}"
- info "CONFIG_PATH=${CONFIG_PATH}"
- info "EMAIL_A=${EMAIL_A}"
- info "DEVICE_A=${DEVICE_A}"
- info "DEVICE_B=${DEVICE_B}"
-
- local token_a token_b
- local bind_a_resp bind_b_resp del_b_resp del_a_resp
- local family_id owner_user_id member_user_id
- local code_delete
- local q_member_before q_member_after q_owner_after q_family_after
-
- bold "步骤1: A设备登录并绑定邮箱A"
- token_a="$(device_login "$DEVICE_A" "qa-cleanup-device-a")"
- bind_a_resp="$(bind_email_with_verification_auto "$token_a" "$EMAIL_A" 1)"
- token_a="$(token_or_keep "$bind_a_resp" "$token_a")"
-
- bold "步骤2: B设备登录并绑定邮箱A加入家庭"
- token_b="$(device_login "$DEVICE_B" "qa-cleanup-device-b")"
- bind_b_resp="$(bind_email_with_verification_auto "$token_b" "$EMAIL_A" 2)"
- token_b="$(token_or_keep "$bind_b_resp" "$token_b")"
-
- family_id="$(jq -r '.data.family_id // ""' <<<"$bind_b_resp")"
- owner_user_id="$(jq -r '.data.owner_user_id // ""' <<<"$bind_b_resp")"
- member_user_id="$(jq -r '.data.user_id // ""' <<<"$bind_b_resp")"
- [[ -n "$family_id" && "$family_id" != "null" ]] || fail "无法获取 family_id: ${bind_b_resp}"
- [[ -n "$owner_user_id" && "$owner_user_id" != "null" ]] || fail "无法获取 owner_user_id: ${bind_b_resp}"
- [[ -n "$member_user_id" && "$member_user_id" != "null" ]] || fail "无法获取 member_user_id: ${bind_b_resp}"
-
- q_member_before="$(db_query_json "SELECT status, role, family_id FROM user_family_member WHERE user_id = ${member_user_id} ORDER BY id DESC LIMIT 1")"
- assert_db_field_equals "$q_member_before" "status" "1"
- info "B注销前家庭状态校验通过(status=1)"
-
- bold "步骤3: 注销设备B账号,检查B离组"
- code_delete="$(get_verify_code "$EMAIL_A" 4)"
- del_b_resp="$(delete_account "$token_b" "$EMAIL_A" "$code_delete")"
- info "B注销结果: $(jq -c '.data' <<<"$del_b_resp")"
-
- q_member_after="$(db_query_json "SELECT status, role, family_id FROM user_family_member WHERE user_id = ${member_user_id} ORDER BY id DESC LIMIT 1")"
- assert_db_field_equals "$q_member_after" "status" "3"
- info "B注销后家庭状态校验通过(status=3)"
-
- bold "步骤4: 注销设备A(家庭主)账号,检查家庭解散"
- code_delete="$(get_verify_code "$EMAIL_A" 4)"
- del_a_resp="$(delete_account "$token_a" "$EMAIL_A" "$code_delete")"
- info "A注销结果: $(jq -c '.data' <<<"$del_a_resp")"
-
- q_owner_after="$(db_query_json "SELECT status, role, family_id FROM user_family_member WHERE user_id = ${owner_user_id} ORDER BY id DESC LIMIT 1")"
- assert_db_field_equals "$q_owner_after" "status" "3"
- info "A注销后主账号家庭状态校验通过(status=3)"
-
- q_family_after="$(db_query_json "SELECT status FROM user_family WHERE id = ${family_id} ORDER BY id DESC LIMIT 1")"
- assert_db_field_equals "$q_family_after" "status" "0"
- info "家庭状态校验通过(status=0)"
-
- bold "校验通过 ✅ 注销后家庭成员与家庭状态已正确更新"
-}
-
-main "$@"
-
diff --git a/script/family_scenarios_34_quick.sh b/script/family_scenarios_34_quick.sh
deleted file mode 100755
index 3e9c301..0000000
--- a/script/family_scenarios_34_quick.sh
+++ /dev/null
@@ -1,335 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
-EMAIL_A="${EMAIL_A:-family34_a_$(date +%s)@example.com}"
-DEVICE_A="${DEVICE_A:-qa34-device-a-$(date +%s)}"
-DEVICE_B="${DEVICE_B:-qa34-device-b-$(date +%s)}"
-SEND_CODE_RETRY_WAIT="${SEND_CODE_RETRY_WAIT:-61}"
-SEND_CODE_MAX_RETRY="${SEND_CODE_MAX_RETRY:-8}"
-VERIFY_CODE_PROVIDER="${VERIFY_CODE_PROVIDER:-}"
-VERIFY_CODE_OVERRIDE="${VERIFY_CODE_OVERRIDE:-}"
-REGISTER_CODE_OVERRIDE="${REGISTER_CODE_OVERRIDE:-}"
-SECURITY_CODE_OVERRIDE="${SECURITY_CODE_OVERRIDE:-}"
-ALLOW_INTERACTIVE_INPUT="${ALLOW_INTERACTIVE_INPUT:-auto}"
-ENABLE_REDIS_CODE_FALLBACK="${ENABLE_REDIS_CODE_FALLBACK:-1}"
-REDIS_CLI_BIN="${REDIS_CLI_BIN:-redis-cli}"
-REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
-REDIS_PORT="${REDIS_PORT:-6379}"
-REDIS_DB="${REDIS_DB:-0}"
-REDIS_PASSWORD="${REDIS_PASSWORD:-}"
-BIND_VERIFY_RETRY="${BIND_VERIFY_RETRY:-1}"
-
-bold() { printf "\033[1m%s\033[0m\n" "$*"; }
-info() { printf "[INFO] %s\n" "$*" >&2; }
-warn() { printf "[WARN] %s\n" "$*" >&2; }
-fail() { printf "[FAIL] %s\n" "$*" >&2; exit 1; }
-
-require_cmd() {
- command -v "$1" >/dev/null 2>&1 || fail "缺少命令: $1"
-}
-
-json_code() { jq -r '.code // ""' <<<"$1"; }
-json_msg() { jq -r '.msg // ""' <<<"$1"; }
-
-can_prompt_input() {
- if [[ "$ALLOW_INTERACTIVE_INPUT" == "1" ]]; then
- return 0
- fi
- if [[ "$ALLOW_INTERACTIVE_INPUT" == "0" ]]; then
- return 1
- fi
- [[ -t 0 ]]
-}
-
-redis_get_key_raw() {
- local key="$1"
- command -v "$REDIS_CLI_BIN" >/dev/null 2>&1 || return 1
-
- local -a args
- args=("$REDIS_CLI_BIN" -h "$REDIS_HOST" -p "$REDIS_PORT" -n "$REDIS_DB" --raw)
- if [[ -n "$REDIS_PASSWORD" ]]; then
- args+=(-a "$REDIS_PASSWORD")
- fi
- "${args[@]}" GET "$key" 2>/dev/null || return 1
-}
-
-get_verify_code_from_redis() {
- local email="$1"
- local typ="$2"
- local scene key raw code
-
- if [[ "$ENABLE_REDIS_CODE_FALLBACK" != "1" ]]; then
- return 1
- fi
-
- case "$typ" in
- 1) scene="register" ;;
- 2) scene="security" ;;
- *) scene="unknown" ;;
- esac
-
- key="auth:verify:email:${scene}:${email}"
- raw="$(redis_get_key_raw "$key" || true)"
- [[ -n "$raw" ]] || return 1
-
- code="$(jq -r '.code // ""' <<<"$raw" 2>/dev/null || true)"
- [[ -n "$code" && "$code" != "null" ]] || return 1
- printf "%s" "$code"
-}
-
-api_request() {
- local method="$1"
- local path="$2"
- local body="${3:-}"
- local token="${4:-}"
-
- local -a args
- args=(-sS -X "$method" "${BASE_URL}${path}" -H "Content-Type: application/json")
- if [[ -n "$token" ]]; then
- args+=(-H "Authorization: ${token}")
- fi
- if [[ "$method" != "GET" ]]; then
- args+=(-d "$body")
- fi
- curl "${args[@]}"
-}
-
-assert_success() {
- local resp="$1"
- local step="$2"
- local code
- code="$(json_code "$resp")"
- if [[ "$code" != "200" ]]; then
- fail "${step} 失败, code=${code}, msg=$(json_msg "$resp"), resp=${resp}"
- fi
-}
-
-token_or_keep() {
- local resp="$1"
- local fallback="$2"
- local token
- token="$(jq -r '.data.token // ""' <<<"$resp")"
- if [[ -n "$token" && "$token" != "null" ]]; then
- printf "%s" "$token"
- else
- printf "%s" "$fallback"
- fi
-}
-
-device_login() {
- local identifier="$1"
- local user_agent="$2"
- local payload resp token
-
- payload="$(jq -nc --arg identifier "$identifier" --arg user_agent "$user_agent" '{identifier:$identifier,user_agent:$user_agent}')"
- resp="$(api_request "POST" "/v1/auth/login/device" "$payload")"
- assert_success "$resp" "设备登录(${identifier})"
- token="$(jq -r '.data.token // ""' <<<"$resp")"
- [[ -n "$token" && "$token" != "null" ]] || fail "设备登录(${identifier})返回空 token: ${resp}"
- printf "%s" "$token"
-}
-
-send_code_with_retry() {
- local email="$1"
- local typ="$2"
- local payload resp code retry
-
- payload="$(jq -nc --arg email "$email" --argjson type "$typ" '{email:$email,type:$type}')"
- retry=0
- while true; do
- resp="$(api_request "POST" "/v1/common/send_code" "$payload")"
- code="$(json_code "$resp")"
- if [[ "$code" == "200" ]]; then
- printf "%s" "$resp"
- return 0
- fi
- if [[ "$code" == "401" ]]; then
- retry=$((retry + 1))
- if (( retry > SEND_CODE_MAX_RETRY )); then
- fail "发送验证码达到重试上限, email=${email}, type=${typ}, resp=${resp}"
- fi
- warn "验证码发送频率限制,等待 ${SEND_CODE_RETRY_WAIT}s 后重试(${retry}/${SEND_CODE_MAX_RETRY})..."
- sleep "${SEND_CODE_RETRY_WAIT}"
- continue
- fi
- fail "发送验证码失败, email=${email}, type=${typ}, code=${code}, msg=$(json_msg "$resp"), resp=${resp}"
- done
-}
-
-get_verify_code() {
- local email="$1"
- local typ="$2"
- local send_resp code
-
- send_resp="$(send_code_with_retry "$email" "$typ")"
- code="$(jq -r '.data.code // ""' <<<"$send_resp")"
- if [[ -n "$code" && "$code" != "null" ]]; then
- info "验证码已从接口返回(type=${typ})"
- printf "%s" "$code"
- return 0
- fi
-
- code="$(get_verify_code_from_redis "$email" "$typ" || true)"
- if [[ -n "$code" ]]; then
- info "验证码已从 Redis 回读(type=${typ})"
- printf "%s" "$code"
- return 0
- fi
-
- if [[ -n "$VERIFY_CODE_PROVIDER" ]]; then
- code="$("$VERIFY_CODE_PROVIDER" "$email" "$typ" || true)"
- if [[ -n "$code" ]]; then
- info "验证码由 VERIFY_CODE_PROVIDER 提供(type=${typ})"
- printf "%s" "$code"
- return 0
- fi
- fi
-
- if [[ "$typ" == "1" && -n "$REGISTER_CODE_OVERRIDE" ]]; then
- printf "%s" "$REGISTER_CODE_OVERRIDE"
- return 0
- fi
- if [[ "$typ" == "2" && -n "$SECURITY_CODE_OVERRIDE" ]]; then
- printf "%s" "$SECURITY_CODE_OVERRIDE"
- return 0
- fi
- if [[ -n "$VERIFY_CODE_OVERRIDE" ]]; then
- printf "%s" "$VERIFY_CODE_OVERRIDE"
- return 0
- fi
-
- if ! can_prompt_input; then
- fail "验证码不可自动获取(type=${typ}, email=${email})"
- fi
- read -r -p "请输入邮箱 ${email} 的验证码(type=${typ}):" code
- [[ -n "$code" ]] || fail "验证码为空"
- printf "%s" "$code"
-}
-
-bind_email_with_verification() {
- local token="$1"
- local email="$2"
- local code="$3"
- local payload
- payload="$(jq -nc --arg email "$email" --arg code "$code" '{email:$email,code:$code}')"
- api_request "POST" "/v1/public/user/bind_email_with_verification" "$payload" "$token"
-}
-
-bind_email_with_verification_auto() {
- local token="$1"
- local email="$2"
- local typ="$3"
- local max_retry="${4:-$BIND_VERIFY_RETRY}"
- local attempt=0 code resp api_code
-
- while true; do
- code="$(get_verify_code "$email" "$typ")"
- resp="$(bind_email_with_verification "$token" "$email" "$code")"
- api_code="$(json_code "$resp")"
- if [[ "$api_code" == "200" ]]; then
- printf "%s" "$resp"
- return 0
- fi
- if [[ "$api_code" == "70001" && "$attempt" -lt "$max_retry" ]]; then
- warn "绑定邮箱验证码校验失败,重新获取验证码重试($((attempt+1))/${max_retry})..."
- attempt=$((attempt + 1))
- continue
- fi
- fail "绑定邮箱(${email}) 失败, code=${api_code}, msg=$(json_msg "$resp"), resp=${resp}"
- done
-}
-
-get_devices() {
- local token="$1"
- local resp
- resp="$(api_request "GET" "/v1/public/user/devices" "" "$token")"
- assert_success "$resp" "获取设备列表"
- printf "%s" "$resp"
-}
-
-find_device_id() {
- local devices_resp="$1"
- local identifier="$2"
- jq -r --arg identifier "$identifier" '.data.list[]? | select(.identifier == $identifier) | .id' <<<"$devices_resp" | head -n1
-}
-
-assert_device_exists() {
- local devices_resp="$1"
- local identifier="$2"
- jq -e --arg identifier "$identifier" '.data.list | any(.identifier == $identifier)' <<<"$devices_resp" >/dev/null \
- || fail "设备列表中未找到 identifier=${identifier}, resp=${devices_resp}"
-}
-
-unbind_device() {
- local token="$1"
- local device_id="$2"
- local payload resp
- payload="$(jq -nc --arg id "$device_id" '{id:($id|tonumber)}')"
- resp="$(api_request "PUT" "/v1/public/user/unbind_device" "$payload" "$token")"
- assert_success "$resp" "解绑设备(id=${device_id})"
-}
-
-assert_token_invalid() {
- local token="$1"
- local who="$2"
- local resp code
- resp="$(api_request "GET" "/v1/public/user/info" "" "$token")"
- code="$(json_code "$resp")"
- if [[ "$code" == "200" ]]; then
- fail "${who} 预期已失效,但 token 仍可访问: ${resp}"
- fi
- info "${who} token 已失效,符合预期(code=${code}, msg=$(json_msg "$resp"))"
-}
-
-main() {
- require_cmd curl
- require_cmd jq
-
- bold "家庭组快速回归: 场景3/4"
- info "BASE_URL=${BASE_URL}"
- info "EMAIL_A=${EMAIL_A}"
- info "DEVICE_A=${DEVICE_A}"
- info "DEVICE_B=${DEVICE_B}"
-
- local token_a token_b token_a2
- local bind_resp devices_resp
- local device_id_a device_id_b
-
- bold "预置: A设备登录并绑定邮箱A"
- token_a="$(device_login "$DEVICE_A" "qa34-device-a")"
- bind_resp="$(bind_email_with_verification_auto "$token_a" "$EMAIL_A" 1)"
- token_a="$(token_or_keep "$bind_resp" "$token_a")"
-
- bold "场景3: B绑定邮箱A后踢A"
- token_b="$(device_login "$DEVICE_B" "qa34-device-b")"
- bind_resp="$(bind_email_with_verification_auto "$token_b" "$EMAIL_A" 2)"
- token_b="$(token_or_keep "$bind_resp" "$token_b")"
- devices_resp="$(get_devices "$token_b")"
- assert_device_exists "$devices_resp" "$DEVICE_A"
- assert_device_exists "$devices_resp" "$DEVICE_B"
- device_id_a="$(find_device_id "$devices_resp" "$DEVICE_A")"
- [[ -n "$device_id_a" ]] || fail "场景3 无法找到设备A ID"
- unbind_device "$token_b" "$device_id_a"
- assert_token_invalid "$token_a" "设备A(被B踢后)"
- info "场景3通过"
-
- bold "场景4: A重登绑定邮箱A后踢B"
- token_a2="$(device_login "$DEVICE_A" "qa34-device-a-relogin")"
- bind_resp="$(bind_email_with_verification_auto "$token_a2" "$EMAIL_A" 2)"
- token_a2="$(token_or_keep "$bind_resp" "$token_a2")"
- devices_resp="$(get_devices "$token_a2")"
- assert_device_exists "$devices_resp" "$DEVICE_A"
- assert_device_exists "$devices_resp" "$DEVICE_B"
- device_id_b="$(find_device_id "$devices_resp" "$DEVICE_B")"
- [[ -n "$device_id_b" ]] || fail "场景4 无法找到设备B ID"
- unbind_device "$token_a2" "$device_id_b"
- assert_token_invalid "$token_b" "设备B(被A踢后)"
- info "场景4通过"
-
- bold "场景3/4 快速回归完成 ✅"
-}
-
-main "$@"
-
diff --git a/script/family_scenarios_regression.sh b/script/family_scenarios_regression.sh
deleted file mode 100755
index 72f2134..0000000
--- a/script/family_scenarios_regression.sh
+++ /dev/null
@@ -1,394 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-BASE_URL="${BASE_URL:-http://127.0.0.1:8080}"
-EMAIL_A="${EMAIL_A:-family_a_$(date +%s)@example.com}"
-DEVICE_A="${DEVICE_A:-qa-device-a-$(date +%s)}"
-DEVICE_B="${DEVICE_B:-qa-device-b-$(date +%s)}"
-DEVICE_C="${DEVICE_C:-qa-device-c-$(date +%s)}"
-EMAIL_LOGIN_CODE="${EMAIL_LOGIN_CODE:-202511}"
-SEND_CODE_RETRY_WAIT="${SEND_CODE_RETRY_WAIT:-61}"
-SEND_CODE_MAX_RETRY="${SEND_CODE_MAX_RETRY:-8}"
-VERIFY_CODE_PROVIDER="${VERIFY_CODE_PROVIDER:-}"
-VERIFY_CODE_OVERRIDE="${VERIFY_CODE_OVERRIDE:-}"
-REGISTER_CODE_OVERRIDE="${REGISTER_CODE_OVERRIDE:-}"
-SECURITY_CODE_OVERRIDE="${SECURITY_CODE_OVERRIDE:-}"
-ALLOW_INTERACTIVE_INPUT="${ALLOW_INTERACTIVE_INPUT:-auto}"
-ENABLE_REDIS_CODE_FALLBACK="${ENABLE_REDIS_CODE_FALLBACK:-1}"
-REDIS_CLI_BIN="${REDIS_CLI_BIN:-redis-cli}"
-REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
-REDIS_PORT="${REDIS_PORT:-6379}"
-REDIS_DB="${REDIS_DB:-0}"
-REDIS_PASSWORD="${REDIS_PASSWORD:-}"
-BIND_VERIFY_RETRY="${BIND_VERIFY_RETRY:-1}"
-
-bold() { printf "\033[1m%s\033[0m\n" "$*"; }
-info() { printf "[INFO] %s\n" "$*" >&2; }
-warn() { printf "[WARN] %s\n" "$*" >&2; }
-fail() { printf "[FAIL] %s\n" "$*" >&2; exit 1; }
-
-require_cmd() {
- command -v "$1" >/dev/null 2>&1 || fail "缺少命令: $1"
-}
-
-json_code() { jq -r '.code // ""' <<<"$1"; }
-json_msg() { jq -r '.msg // ""' <<<"$1"; }
-
-can_prompt_input() {
- if [[ "$ALLOW_INTERACTIVE_INPUT" == "1" ]]; then
- return 0
- fi
- if [[ "$ALLOW_INTERACTIVE_INPUT" == "0" ]]; then
- return 1
- fi
- [[ -t 0 ]]
-}
-
-redis_get_key_raw() {
- local key="$1"
- command -v "$REDIS_CLI_BIN" >/dev/null 2>&1 || return 1
-
- local -a args
- args=("$REDIS_CLI_BIN" -h "$REDIS_HOST" -p "$REDIS_PORT" -n "$REDIS_DB" --raw)
- if [[ -n "$REDIS_PASSWORD" ]]; then
- args+=(-a "$REDIS_PASSWORD")
- fi
-
- "${args[@]}" GET "$key" 2>/dev/null || return 1
-}
-
-get_verify_code_from_redis() {
- local email="$1"
- local typ="$2"
- local scene key raw code
-
- if [[ "$ENABLE_REDIS_CODE_FALLBACK" != "1" ]]; then
- return 1
- fi
-
- case "$typ" in
- 1) scene="register" ;;
- 2) scene="security" ;;
- *) scene="unknown" ;;
- esac
-
- key="auth:verify:email:${scene}:${email}"
- raw="$(redis_get_key_raw "$key" || true)"
- [[ -n "$raw" ]] || return 1
-
- code="$(jq -r '.code // ""' <<<"$raw" 2>/dev/null || true)"
- [[ -n "$code" && "$code" != "null" ]] || return 1
-
- printf "%s" "$code"
- return 0
-}
-
-api_request() {
- local method="$1"
- local path="$2"
- local body="${3:-}"
- local token="${4:-}"
-
- local -a args
- args=(-sS -X "$method" "${BASE_URL}${path}" -H "Content-Type: application/json")
- if [[ -n "$token" ]]; then
- args+=(-H "Authorization: ${token}")
- fi
- if [[ "$method" != "GET" ]]; then
- args+=(-d "$body")
- fi
- curl "${args[@]}"
-}
-
-assert_success() {
- local resp="$1"
- local step="$2"
- local code
- code="$(json_code "$resp")"
- if [[ "$code" != "200" ]]; then
- fail "${step} 失败, code=${code}, msg=$(json_msg "$resp"), resp=${resp}"
- fi
-}
-
-token_or_keep() {
- local resp="$1"
- local fallback="$2"
- local token
- token="$(jq -r '.data.token // ""' <<<"$resp")"
- if [[ -n "$token" && "$token" != "null" ]]; then
- printf "%s" "$token"
- else
- printf "%s" "$fallback"
- fi
-}
-
-device_login() {
- local identifier="$1"
- local user_agent="$2"
- local payload resp token
-
- payload="$(jq -nc --arg identifier "$identifier" --arg user_agent "$user_agent" '{identifier:$identifier,user_agent:$user_agent}')"
- resp="$(api_request "POST" "/v1/auth/login/device" "$payload")"
- assert_success "$resp" "设备登录(${identifier})"
-
- token="$(jq -r '.data.token // ""' <<<"$resp")"
- [[ -n "$token" && "$token" != "null" ]] || fail "设备登录(${identifier})返回空 token: ${resp}"
- printf "%s" "$token"
-}
-
-email_login() {
- local email="$1"
- local code="$2"
- local payload resp token
-
- payload="$(jq -nc --arg email "$email" --arg code "$code" --arg identifier "web-login-$(date +%s)" --arg user_agent "qa-web" '{email:$email,code:$code,identifier:$identifier,user_agent:$user_agent}')"
- resp="$(api_request "POST" "/v1/auth/login/email" "$payload")"
- assert_success "$resp" "邮箱登录(${email})"
-
- token="$(jq -r '.data.token // ""' <<<"$resp")"
- [[ -n "$token" && "$token" != "null" ]] || fail "邮箱登录(${email})返回空 token: ${resp}"
- printf "%s" "$token"
-}
-
-send_code_with_retry() {
- local email="$1"
- local typ="$2"
- local payload resp code retry
-
- payload="$(jq -nc --arg email "$email" --argjson type "$typ" '{email:$email,type:$type}')"
- retry=0
- while true; do
- resp="$(api_request "POST" "/v1/common/send_code" "$payload")"
- code="$(json_code "$resp")"
- if [[ "$code" == "200" ]]; then
- printf "%s" "$resp"
- return 0
- fi
- if [[ "$code" == "401" ]]; then
- retry=$((retry + 1))
- if (( retry > SEND_CODE_MAX_RETRY )); then
- fail "发送验证码达到重试上限, email=${email}, type=${typ}, resp=${resp}"
- fi
- warn "验证码发送频率限制,等待 ${SEND_CODE_RETRY_WAIT}s 后重试(${retry}/${SEND_CODE_MAX_RETRY})..."
- sleep "${SEND_CODE_RETRY_WAIT}"
- continue
- fi
- fail "发送验证码失败, email=${email}, type=${typ}, code=${code}, msg=$(json_msg "$resp"), resp=${resp}"
- done
-}
-
-get_verify_code() {
- local email="$1"
- local typ="$2"
- local send_resp code
-
- send_resp="$(send_code_with_retry "$email" "$typ")"
- code="$(jq -r '.data.code // ""' <<<"$send_resp")"
- if [[ -n "$code" && "$code" != "null" ]]; then
- info "验证码已从接口返回(type=${typ})"
- printf "%s" "$code"
- return 0
- fi
-
- code="$(get_verify_code_from_redis "$email" "$typ" || true)"
- if [[ -n "$code" ]]; then
- info "验证码已从 Redis 回读(type=${typ})"
- printf "%s" "$code"
- return 0
- fi
-
- if [[ -n "$VERIFY_CODE_PROVIDER" ]]; then
- code="$("$VERIFY_CODE_PROVIDER" "$email" "$typ" || true)"
- if [[ -n "$code" ]]; then
- info "验证码由 VERIFY_CODE_PROVIDER 提供(type=${typ})"
- printf "%s" "$code"
- return 0
- fi
- fi
-
- if [[ "$typ" == "1" && -n "$REGISTER_CODE_OVERRIDE" ]]; then
- info "使用 REGISTER_CODE_OVERRIDE 作为验证码(type=${typ})"
- printf "%s" "$REGISTER_CODE_OVERRIDE"
- return 0
- fi
-
- if [[ "$typ" == "2" && -n "$SECURITY_CODE_OVERRIDE" ]]; then
- info "使用 SECURITY_CODE_OVERRIDE 作为验证码(type=${typ})"
- printf "%s" "$SECURITY_CODE_OVERRIDE"
- return 0
- fi
-
- if [[ -n "$VERIFY_CODE_OVERRIDE" ]]; then
- info "使用 VERIFY_CODE_OVERRIDE 作为验证码(type=${typ})"
- printf "%s" "$VERIFY_CODE_OVERRIDE"
- return 0
- fi
-
- if ! can_prompt_input; then
- fail "验证码不可自动获取(type=${typ}, email=${email})。请设置 VERIFY_CODE_PROVIDER 或 REGISTER_CODE_OVERRIDE/SECURITY_CODE_OVERRIDE,或开启交互输入(ALLOW_INTERACTIVE_INPUT=1)"
- fi
-
- read -r -p "请输入邮箱 ${email} 的验证码(type=${typ}):" code
- [[ -n "$code" ]] || fail "验证码为空"
- printf "%s" "$code"
-}
-
-bind_email_with_verification() {
- local token="$1"
- local email="$2"
- local code="$3"
- local payload resp
-
- payload="$(jq -nc --arg email "$email" --arg code "$code" '{email:$email,code:$code}')"
- resp="$(api_request "POST" "/v1/public/user/bind_email_with_verification" "$payload" "$token")"
- printf "%s" "$resp"
-}
-
-bind_email_with_verification_auto() {
- local token="$1"
- local email="$2"
- local typ="$3"
- local max_retry="${4:-$BIND_VERIFY_RETRY}"
- local attempt=0 code resp api_code
-
- while true; do
- code="$(get_verify_code "$email" "$typ")"
- resp="$(bind_email_with_verification "$token" "$email" "$code")"
- api_code="$(json_code "$resp")"
- if [[ "$api_code" == "200" ]]; then
- printf "%s" "$resp"
- return 0
- fi
- if [[ "$api_code" == "70001" && "$attempt" -lt "$max_retry" ]]; then
- warn "绑定邮箱验证码校验失败,重新获取验证码重试($((attempt+1))/${max_retry})..."
- attempt=$((attempt + 1))
- continue
- fi
- fail "绑定邮箱(${email}) 失败, code=${api_code}, msg=$(json_msg "$resp"), resp=${resp}"
- done
-}
-
-get_devices() {
- local token="$1"
- local resp
- resp="$(api_request "GET" "/v1/public/user/devices" "" "$token")"
- assert_success "$resp" "获取设备列表"
- printf "%s" "$resp"
-}
-
-find_device_id() {
- local devices_resp="$1"
- local identifier="$2"
- jq -r --arg identifier "$identifier" '.data.list[]? | select(.identifier == $identifier) | .id' <<<"$devices_resp" | head -n1
-}
-
-assert_device_exists() {
- local devices_resp="$1"
- local identifier="$2"
- jq -e --arg identifier "$identifier" '.data.list | any(.identifier == $identifier)' <<<"$devices_resp" >/dev/null \
- || fail "设备列表中未找到 identifier=${identifier}, resp=${devices_resp}"
-}
-
-unbind_device() {
- local token="$1"
- local device_id="$2"
- local payload resp
-
- payload="$(jq -nc --arg id "$device_id" '{id:($id|tonumber)}')"
- resp="$(api_request "PUT" "/v1/public/user/unbind_device" "$payload" "$token")"
- assert_success "$resp" "解绑设备(id=${device_id})"
-}
-
-assert_token_invalid() {
- local token="$1"
- local who="$2"
- local resp code
-
- resp="$(api_request "GET" "/v1/public/user/info" "" "$token")"
- code="$(json_code "$resp")"
- if [[ "$code" == "200" ]]; then
- fail "${who} 预期已下线/失效,但 token 仍可访问: ${resp}"
- fi
- info "${who} token 已失效,符合预期(code=${code}, msg=$(json_msg "$resp"))"
-}
-
-main() {
- require_cmd curl
- require_cmd jq
-
- bold "家庭组场景回归脚本"
- info "BASE_URL=${BASE_URL}"
- info "EMAIL_A=${EMAIL_A}"
- info "DEVICE_A=${DEVICE_A}"
- info "DEVICE_B=${DEVICE_B}"
- info "DEVICE_C=${DEVICE_C} (本脚本当前未使用)"
- info "EMAIL_LOGIN_CODE=${EMAIL_LOGIN_CODE}"
- info "ALLOW_INTERACTIVE_INPUT=${ALLOW_INTERACTIVE_INPUT}"
- info "ENABLE_REDIS_CODE_FALLBACK=${ENABLE_REDIS_CODE_FALLBACK}"
- info "BIND_VERIFY_RETRY=${BIND_VERIFY_RETRY}"
-
- local token_a token_b token_a2 token_b2 token_web
- local bind_resp devices_resp
- local device_id_a device_id_b device_id_b2
-
- bold "场景1: 设备A登录并绑定邮箱A"
- token_a="$(device_login "$DEVICE_A" "qa-device-a")"
- bind_resp="$(bind_email_with_verification_auto "$token_a" "$EMAIL_A" 1)"
- token_a="$(token_or_keep "$bind_resp" "$token_a")"
- devices_resp="$(get_devices "$token_a")"
- assert_device_exists "$devices_resp" "$DEVICE_A"
- device_id_a="$(find_device_id "$devices_resp" "$DEVICE_A")"
- [[ -n "$device_id_a" ]] || fail "场景1 无法找到设备A ID"
- info "场景1完成: deviceA.id=${device_id_a}"
-
- bold "场景2: 邮箱A网页登录"
- token_web="$(email_login "$EMAIL_A" "$EMAIL_LOGIN_CODE")"
- [[ -n "$token_web" ]] || fail "场景2 邮箱登录 token 为空"
- info "场景2完成: 邮箱登录成功"
-
- bold "场景3: 设备B登录并绑定邮箱A,然后B踢A"
- token_b="$(device_login "$DEVICE_B" "qa-device-b")"
- bind_resp="$(bind_email_with_verification_auto "$token_b" "$EMAIL_A" 2)"
- token_b="$(token_or_keep "$bind_resp" "$token_b")"
- jq -e '.data.family_joined == true' <<<"$bind_resp" >/dev/null || warn "场景3: family_joined 非 true,resp=${bind_resp}"
-
- devices_resp="$(get_devices "$token_b")"
- assert_device_exists "$devices_resp" "$DEVICE_A"
- assert_device_exists "$devices_resp" "$DEVICE_B"
- device_id_a="$(find_device_id "$devices_resp" "$DEVICE_A")"
- [[ -n "$device_id_a" ]] || fail "场景3 无法从B视角找到A设备ID"
- unbind_device "$token_b" "$device_id_a"
- assert_token_invalid "$token_a" "设备A(被B踢后)"
- info "场景3完成: B已踢A"
-
- bold "场景4: 设备A重新登录并绑定邮箱A,然后A踢B"
- token_a2="$(device_login "$DEVICE_A" "qa-device-a-relogin")"
- bind_resp="$(bind_email_with_verification_auto "$token_a2" "$EMAIL_A" 2)"
- token_a2="$(token_or_keep "$bind_resp" "$token_a2")"
-
- devices_resp="$(get_devices "$token_a2")"
- assert_device_exists "$devices_resp" "$DEVICE_A"
- assert_device_exists "$devices_resp" "$DEVICE_B"
- device_id_b="$(find_device_id "$devices_resp" "$DEVICE_B")"
- [[ -n "$device_id_b" ]] || fail "场景4 无法从A视角找到B设备ID"
- unbind_device "$token_a2" "$device_id_b"
- assert_token_invalid "$token_b" "设备B(被A踢后)"
- info "场景4完成: A已踢B"
-
- bold "场景5: 设备B登录后执行退出(删除当前设备)"
- token_b2="$(device_login "$DEVICE_B" "qa-device-b-relogin")"
- bind_resp="$(bind_email_with_verification_auto "$token_b2" "$EMAIL_A" 2)"
- token_b2="$(token_or_keep "$bind_resp" "$token_b2")"
-
- devices_resp="$(get_devices "$token_b2")"
- device_id_b2="$(find_device_id "$devices_resp" "$DEVICE_B")"
- [[ -n "$device_id_b2" ]] || fail "场景5 无法找到当前设备B ID"
- unbind_device "$token_b2" "$device_id_b2"
- assert_token_invalid "$token_b2" "设备B(主动退出后)"
- info "场景5完成: B退出后 token 失效"
-
- bold "全部场景执行完成 ✅"
-}
-
-main "$@"
diff --git a/script/generate.sh b/script/generate.sh
deleted file mode 100755
index 26fd79a..0000000
--- a/script/generate.sh
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/bin/bash
-
-OS_TYPE=$(uname)
-ARCH_TYPE=$(uname -m)
-
-if [[ "$OS_TYPE" == "Linux" ]]; then
- echo "The current operating system is Linux"
- if [[ "$ARCH_TYPE" == "x86_64" ]]; then
- echo "Format api file"
- ./generate/gopure-linux-amd64 api format --dir ./apis
- echo "Architecture: amd64"
- ./generate/gopure-linux-amd64 api go -api *.api -dir . -style goZero
- elif [[ "$ARCH_TYPE" == "aarch64" ]]; then
- echo "Format api file"
- ./generate/gopure-linux-arm64 api format --dir ./apis
- echo "Architecture: arm64"
- ./generate/gopure-linux-arm64 api go -api *.api -dir . -style goZero
- else
- echo "Unrecognized architecture: $ARCH_TYPE"
- fi
-elif [[ "$OS_TYPE" == "Darwin" ]]; then
- echo "The current operating system is macOS"
- if [[ "$ARCH_TYPE" == "x86_64" ]]; then
- echo "Format api file"
- ./generate/gopure-darwin-amd64 api format --dir ./apis
- echo "Architecture: amd64"
- ./generate/gopure-darwin-amd64 api go -api *.api -dir . -style goZero
- elif [[ "$ARCH_TYPE" == "arm64" ]]; then
- echo "Format api file"
- ./generate/gopure-darwin-arm64 api format --dir ./apis
- echo "Architecture: arm64"
- ./generate/gopure-darwin-arm64 api go -api *.api -dir . -style goZero
- else
- echo "Unrecognized architecture: $ARCH_TYPE"
- fi
-elif [[ "$OS_TYPE" == "CYGWIN"* || "$OS_TYPE" == "MINGW"* ]]; then
- echo "The current operating system is Windows"
- if [[ "$ARCH_TYPE" == "x86_64" ]]; then
- echo "Format api file"
- ./generate/gopure-amd64.exe api format --dir ./apis
- echo "Architecture: amd64"
- ./generate/gopure-amd64.exe api go -api *.api -dir . -style goZero
- elif [[ "$ARCH_TYPE" == "arm64" ]]; then
- echo "Format api file"
- ./generate/gopure-arm64.exe api format --dir ./apis
- echo "Architecture: arm64"
- ./generate/gopure-arm64.exe api go -api *.api -dir . -style goZero
- else
- echo "Unrecognized architecture: $ARCH_TYPE"
- fi
-else
- echo "Unrecognized operating system: $OS_TYPE"
-fi
diff --git a/script/install.sh b/script/install.sh
deleted file mode 100644
index 27f102c..0000000
--- a/script/install.sh
+++ /dev/null
@@ -1,96 +0,0 @@
-#!/bin/bash
-
-# 检查是否以 root 用户运行
-if [ "$(id -u)" -ne 0 ]; then
- echo "请以 root 用户运行此脚本"
- exit 1
-fi
-
-# 系统检测,确定使用的包管理工具
-if [ -f /etc/debian_version ]; then
- # Ubuntu / Debian 系统
- PKG_MANAGER="apt-get"
-elif [ -f /etc/redhat-release ]; then
- # CentOS 系统
- PKG_MANAGER="yum"
-else
- echo "不支持的系统"
- exit 1
-fi
-
-# 检查 jq 是否已安装,若未安装则自动安装
-if ! command -v jq &> /dev/null; then
- echo "jq 未安装,正在安装 jq ..."
- if [ "$PKG_MANAGER" == "apt-get" ]; then
- apt-get update && apt-get install -y jq
- elif [ "$PKG_MANAGER" == "yum" ]; then
- yum install -y jq
- else
- echo "无法安装 jq,未知的包管理器"
- exit 1
- fi
-fi
-
-# 获取最新的版本号
-VERSION=$(curl -s https://api.github.com/repos/perfect-panel/ppanel/releases/latest | jq -r .tag_name)
-
-if [ "$VERSION" == "null" ]; then
- echo "无法获取最新版本号,请检查网络或 GitHub API 状态"
- exit 1
-fi
-
-# 安装路径
-INSTALL_DIR="/opt/ppanel-server"
-SERVICE_NAME="ppanel"
-
-# 下载并解压二进制文件
-echo "开始下载 ppanel 二进制文件,版本:$VERSION ..."
-wget https://github.com/perfect-panel/ppanel/releases/download/$VERSION/ppanel-server-linux-amd64.tar.gz -O /tmp/ppanel-server-linux-amd64.tar.gz
-
-# 创建安装目录
-if [ ! -d "$INSTALL_DIR" ]; then
- mkdir -p "$INSTALL_DIR"
-fi
-
-# 解压文件到安装目录
-echo "解压文件到 $INSTALL_DIR ..."
-tar -zxvf /tmp/ppanel-server-linux-amd64.tar.gz -C "$INSTALL_DIR" --strip-components=1
-
-# 给二进制文件赋予执行权限
-chmod +x "$INSTALL_DIR/ppanel-server"
-
-# 创建 systemd 服务文件
-echo "创建 systemd 服务文件 ..."
-cat > /etc/systemd/system/$SERVICE_NAME.service <