From d220516183ad4f69e4fa6070acf3501dffa693b4 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Tue, 3 Mar 2026 09:37:38 -0800 Subject: [PATCH] chore: stop tracking local-only files --- .claude/plan/sync-features.md | 241 ----------- .claude/team-plan/sync-remaining.md | 168 -------- .github/environments/production.yml | 27 -- .github/environments/staging.yml | 23 - .github/workflows/deploy-linux.yml | 79 ---- .gitignore | 7 +- ...ld github.com_perfect-panel_server.run.xml | 12 - script/db_query.go | 78 ---- script/family_delete_account_cleanup_check.sh | 346 --------------- script/family_scenarios_34_quick.sh | 335 --------------- script/family_scenarios_regression.sh | 394 ------------------ script/generate.sh | 53 --- script/install.sh | 96 ----- 13 files changed, 6 insertions(+), 1853 deletions(-) delete mode 100644 .claude/plan/sync-features.md delete mode 100644 .claude/team-plan/sync-remaining.md delete mode 100644 .github/environments/production.yml delete mode 100644 .github/environments/staging.yml delete mode 100644 .github/workflows/deploy-linux.yml delete mode 100644 .run/go build github.com_perfect-panel_server.run.xml delete mode 100644 script/db_query.go delete mode 100755 script/family_delete_account_cleanup_check.sh delete mode 100755 script/family_scenarios_34_quick.sh delete mode 100755 script/family_scenarios_regression.sh delete mode 100755 script/generate.sh delete mode 100644 script/install.sh 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 <