Compare commits

..

430 Commits

Author SHA1 Message Date
EUForest 7d46b31866 fix bug: delete user subscribe 2026-02-14 00:41:24 +08:00
EUForest 31e75efacb Merge upstream/master into master 2026-02-13 23:06:43 +08:00
EUForest 2d4d926924 Merge branch 'master' of https://github.com/OmnTeam/ppanel-server 2026-02-13 21:41:36 +08:00
EUForest 7197f5dcf6 feat(stripe): update webhook event construction to ignore API version mismatches 2026-02-11 17:29:53 +08:00
EUForest 9ad602aabe fix(migration): remove IF NOT EXISTS for MySQL compatibility
Remove IF NOT EXISTS clause from CREATE INDEX statement to ensure
compatibility with older MySQL versions. Migration framework ensures
each migration runs only once, making the clause unnecessary.
2026-02-10 00:45:17 +08:00
EUForest bacdf2f301 fix(migration): correct SQL syntax for traffic_log index creation
Change ALTER TABLE ADD INDEX IF NOT EXISTS to CREATE INDEX IF NOT EXISTS
to comply with MySQL syntax requirements. The IF NOT EXISTS clause is
only supported with CREATE INDEX statement, not with ALTER TABLE ADD INDEX.
2026-02-10 00:25:40 +08:00
EUForest 0883fb9370 fix(ci): handle existing releases and assets in deploy workflow
- Check if release exists before creating
- Delete old assets if they exist
- Use --clobber flag to overwrite existing assets
- Prevent 'already exists' errors on tag force-push
2026-02-09 01:12:20 +08:00
EUForest 34372fe0b3 fix(redemption): enhance redemption code flow with transaction safety and idempotency
This commit addresses critical issues in the redemption code activation flow
to ensure data consistency, prevent duplicate redemptions, and improve user
experience.

Key improvements:

1. Transaction Safety (P0)
   - Wrap subscription creation, used count update, and record insertion in
     a single database transaction
   - Ensure atomicity: all operations succeed or all rollback
   - Prevent orphaned records and data inconsistencies

2. Idempotency Protection (P0)
   - Add redemption record check before processing to prevent duplicate
     operations on queue task retries
   - Maintain idempotency at multiple layers: interface, order, and record

3. Distributed Lock (P1)
   - Implement Redis-based distributed lock (10s timeout) to prevent
     concurrent duplicate redemptions
   - Lock key format: redemption_lock:{user_id}:{code}

4. IsNew Field Correction (P2)
   - Fix IsNew field to correctly determine first-time purchases using
     IsUserEligibleForNewOrder method
   - Ensure accurate statistics and future commission calculations

5. Quota Pre-check (P2)
   - Add quota validation at interface layer for immediate user feedback
   - Prevent "processing" status followed by eventual failure

6. Extended Cache TTL (P2)
   - Increase Redis cache expiration from 30 minutes to 2 hours
   - Ensure queue tasks can retrieve redemption data even with delays

7. Error Handling (P2)
   - Clean up Order records when Redis cache or queue enqueue fails
   - Prevent orphaned Order records in the database

8. Cache Clearing Optimization
   - Add user subscription cache clearing after activation
   - Ensure both node-side and user-side display latest subscription info

Technical details:
- Modified: internal/logic/public/redemption/redeemCodeLogic.go
- Modified: queue/logic/order/activateOrderLogic.go
- Modified: internal/model/redemption/default.go (transaction support)

Testing:
- All changes compiled successfully
- Comprehensive flow verification completed
- Ready for production deployment

BREAKING CHANGE: None
2026-02-09 01:07:39 +08:00
EUForest 8022710720 update: config file 2026-02-08 21:27:40 +08:00
Chang lue Tsen b6a1739efa refactor(routes): rename server group router for clarity and consistency 2026-02-08 06:47:06 -05:00
Chang lue Tsen ea94f3c9f9 feat(stripe): update webhook event construction to ignore API version mismatches 2026-02-08 06:43:34 -05:00
Chang lue Tsen 701d47ed21 feat(order): add validation for quantity and amount limits in order requests 2026-02-08 06:41:23 -05:00
EUForest 7e08a07e29 fix: resolve balance payment issue with coupon and fee calculation
This commit fixes the inconsistent calculation logic between order preview
and actual order creation, which caused balance payment failures when using
coupons.

Changes:
- Standardized fee calculation order in both preCreateOrderLogic and purchaseLogic
- Moved gift amount deduction after fee calculation to ensure correct total
- Removed premature gift amount deduction before transaction in purchaseLogic
- Gift amount is now only deducted within the database transaction

The calculation order is now unified:
1. Apply coupon discount
2. Calculate handling fee based on post-coupon amount
3. Deduct gift amount from total (including fee)

This ensures the preview amount matches the actual payment amount.
2026-02-06 23:14:04 +08:00
EUForest 64023dfd1d update: redis config 2026-01-31 12:08:56 +08:00
EUForest 37200698ab update: redis config 2026-01-31 12:08:12 +08:00
EUForest ffe589ff77 feat: Device short code lookup 2026-01-31 11:52:09 +08:00
greatsage 5f1a546bbe fix: resolve trial subscription cache issue on new user registration
When new users register with trial subscription enabled, the subscription
link fails to connect in Clash clients. This is caused by missing cache
invalidation after transaction commit.

Changes:
- Add cache clearing after successful trial subscription creation
- Clear user subscription cache, subscription details cache, and server cache
- Modify activeTrial functions to return subscription object for cache clearing
- Apply fix to all registration methods: email, phone, device, and OAuth

This ensures subscription links work immediately after registration without
requiring manual subscription reset.
2026-01-22 23:57:15 +07:00
EUForest 5f55b1242e fix: resolve order queue loss issue with retry mechanism and idempotency
- Fix task error handling: return actual errors instead of nil to enable retry
- Add idempotency check: skip processing for already finished orders
- Extend temp order cache: increase from 15 minutes to 24 hours
- Configure retry policy: add MaxRetry(5) for all payment callbacks (Epay, Alipay, Stripe)

This fixes the critical issue where paid orders were being lost due to:
1. Failed tasks being marked as successful and deleted from queue
2. Temporary order info expiring before queue processing
3. No retry mechanism for transient failures

Changes:
- queue/logic/order/activateOrderLogic.go: Fix error returns and add idempotency
- internal/logic/public/portal/purchaseLogic.go: Extend cache to 24 hours
- internal/logic/notify/*NotifyLogic.go: Add retry configuration
2026-01-12 18:30:42 +08:00
EUForest 7d4a19c9a3 fix: add quota limit check to prevent subscription bypass
- Add quota check in preCreateOrderLogic for order preview
- Move quota check inside transaction in purchaseLogic to prevent race condition
- Add quota check in activateOrderLogic as final safeguard when creating subscription
- Add quota check in redeemCodeLogic when redeeming codes for new subscriptions
2026-01-10 21:18:26 +08:00
EUForest 2a1ae2e1cc feat: add index on user.refer_code for faster invite code lookup
- Add idx_refer_code index to improve query performance
- Prevents full table scan when validating invite codes during registration
2026-01-10 20:37:24 +08:00
EUForest 3359704a45 feat: add short_code field to device login API
- Add optional short_code parameter to DeviceLoginRequest
- Add ShortCode field to Device model
- Save short_code to database during device registration
- Add database migration for user_device.short_code column
- Fix duplicate variable declaration in routes.go
2026-01-10 18:11:24 +08:00
Chang lue Tsen 1995987f00 feat(config): add PaddingScheme to configuration and update server config logic 2026-01-07 09:39:10 -05:00
Chang lue Tsen 7d84cf858b fix(currency): initialize exchange rate to 0 and improve error logging in purchase checkout 2026-01-07 09:35:52 -05:00
Chang lue Tsen 58caa497c3 fix(exchange): update exchange rate API endpoint and adjust request parameters 2026-01-07 09:35:09 -05:00
EUForest ed669d0620 feat: remove v prefix from service version and disable SECRET_KEY check 2026-01-07 14:40:07 +08:00
EUForest 076e5e584b fix: use underscore in build time to avoid ldflags parsing error 2026-01-07 14:27:41 +08:00
EUForest d3e18af08e fix: correct ldflags for version and build time 2026-01-07 14:25:55 +08:00
EUForest 69ec491d0a fix bug: Restore WS connection 2026-01-07 01:23:40 +08:00
EUForest d2e9a837cc Merge upstream changes and release v1.3.1 2026-01-06 18:53:52 +08:00
EUForest f452838c63 Merge upstream/master into develop 2026-01-06 18:52:28 +08:00
EUForest 3eb40bd5e4 chore: simplify build workflow for v1.3 2026-01-06 17:10:41 +08:00
EUForest 8a804eec0c chore: simplify build workflow for v1.3 2026-01-06 17:02:31 +08:00
EUForest 8f783b162c feat: redemption code 2026-01-06 16:24:24 +08:00
EUForest 24c7fc8857 add: init sql 2026-01-06 16:16:42 +08:00
EUForest 23ef9dbff1 feat: bind device limit 2026-01-06 16:15:47 +08:00
EUForest ec0a0f968e up: Redemption Code 2026-01-06 16:15:24 +08:00
EUForest 3f3b0ae6ad up: Redemption Code 2026-01-06 16:15:10 +08:00
EUForest 518595b058 feat: Redemption Code 2026-01-05 17:53:31 +08:00
EUForest 5beff61e91 Merge upstream/master into develop
Sync upstream changes from perfect-panel/server

  Includes updates from v1.0.1 to v1.2.5:
  - Currency configuration support
  - Subscribe improvements (short token, inventory check, etc.)
  - Node management enhancements
  - Database migrations
  - Bug fixes and optimizations
2026-01-03 23:21:41 +08:00
Tension 90e2f24d46 fix(config): conditionally set SubscribePath based on gateway mode 2026-01-03 18:05:17 +08:00
EUForest 80ee9a6acf Merge upstream/master into develop
Sync upstream changes from perfect-panel/server

  Includes updates from v1.0.1 to v1.2.5:
  - Currency configuration support
  - Subscribe improvements (short token, inventory check, etc.)
  - Node management enhancements
  - Database migrations
  - Bug fixes and optimizations
2026-01-02 12:51:55 +08:00
Tension 798fb9e245 feat(currency): add currency configuration support and integrate into payment processing 2025-12-31 11:47:24 +08:00
Tension 780e71441d fix(subscribe): refactor getSubscribeV2URL to remove token parameter and adjust URL construction 2025-12-31 10:53:50 +08:00
Tension 24f3c29fad fix(subscribe): improve short token validation by adding case-insensitive comparison 2025-12-30 16:23:07 +08:00
Tension 77a5373d44 feat(adapter): add support for additional parameters in Adapter and Client structs 2025-12-30 14:39:15 +08:00
Tension 8436c2d6ee feat(subscribe): add short token generation for user subscriptions 2025-12-30 14:06:46 +08:00
Tension ff2fa573a0 fix(subscribe): enhance node subscription logic to handle empty tags and log node counts 2025-12-29 15:12:13 +08:00
Tension d4c6aa0528 feat(node): add enabled field to node creation logic 2025-12-29 15:03:39 +08:00
Tension 532a5ab009 feat(config): update subscribe path in global config response 2025-12-29 15:00:19 +08:00
Tension e8084e9d2c feat(subscribe): rename stop user subscribe handler to toggle and update logic for status change 2025-12-29 13:50:50 +08:00
Tension 577490749c feat(subscribe): update inventory logic in subscribe table and add migration scripts 2025-12-29 13:29:12 +08:00
Tension 518294a528 feat(database): add migration to drop server_group table 2025-12-28 22:55:38 +08:00
Tension fb6adc9ae3 feat(subscribe): add inventory check and update logic for subscription plans 2025-12-28 22:44:14 +08:00
Tension 7a2000f696 feat(discount): change discount type to float64 for improved precision 2025-12-28 22:04:50 +08:00
Tension 495c4529ed fix(gorm): adjust caller skip for logging methods to improve stack trace accuracy 2025-12-28 21:52:54 +08:00
Tension bbc3703404 feat(traffic): enhance logging for successful push traffic tasks 2025-12-28 21:52:42 +08:00
Tension ec829452c1 feat(traffic): add debug logging for current time traffic multiplier 2025-12-28 21:52:29 +08:00
Tension 21f77e141b feat(node): update Node Multiplier configuration and initialize node 2025-12-28 21:52:16 +08:00
Tension d332e760f8 feat(subscribe): add ShowOriginalPrice field and related database changes 2025-12-28 17:08:26 +08:00
Tension 31e634ba66 feat(user): add handlers and logic for resetting user subscribe token and traffic 2025-12-28 16:49:28 +08:00
Chang lue Tsen 67f16ead82 feat(user): add unscoped filter to include soft-deleted records in user queries 2025-12-27 10:57:34 -05:00
Chang lue Tsen a9c832cb7c feat(user): implement soft deletion for user accounts and update related logic 2025-12-27 10:45:28 -05:00
Chang lue Tsen e027cbb5de refactor(server): remove server table 2025-12-23 07:52:38 -05:00
Chang lue Tsen ec510b66fb refactor(server): remove deprecated server types and related methods for cleaner codebase 2025-12-23 07:48:03 -05:00
EUForest 47c41d1d14 fix bug: telephone login 2025-12-19 22:46:48 +08:00
EUForest 39db154e53 fix bug: Flag free trial 2025-12-17 22:09:46 +08:00
EUForest 76ff9a658d Merge branch perfect-panel/master/server into develop 2025-12-11 23:53:32 +08:00
Tension 0e7cbf4396 fix(payment): update notification URL construction for gateway mode support 2025-12-08 16:09:21 +08:00
Tension 5d632608ab fix(purchase): update notification URL construction for gateway mode support 2025-12-08 15:47:45 +08:00
Tension 338d962618 fix(api): remove default value for size in QueryAnnouncementRequest 2025-12-01 19:01:24 +08:00
Chang lue Tsen f1794b26b1 revert(swagger): remove basepath from Swagger file generation for improved compatibility 2025-11-30 11:07:17 -05:00
Chang lue Tsen 4cd24e7600 feat(swagger): add basepath to Swagger file generation for improved API routing 2025-11-30 11:02:02 -05:00
Chang lue Tsen e18809f9b7 feat(report): update registration response structure and enhance error logging 2025-11-30 10:28:11 -05:00
Tension 143445a2fc feat(commission): implement commission withdrawal logic and logging 2025-11-29 14:33:05 +08:00
Chang lue Tsen 7277438b07 feat(user): add commission withdrawal and query withdrawal log functionality 2025-11-26 12:13:33 -05:00
Chang lue Tsen 5c2d0be8e2 feat(adapter): add additional protocol parameters for enhanced configuration 2025-11-24 10:43:10 -05:00
Chang lue Tsen 429e535dd4 feat(user): add endpoint and logic for updating user rules 2025-11-24 09:05:49 -05:00
EUForest 5e46357104 修复工作流条件判断
- 移除 startsWith 函数在 if 条件中的使用
- 使用正确的 github.ref_type 判断标签推送
2025-11-24 18:43:25 +08:00
EUForest 35f60df62e 重新触发构建测试 2025-11-24 18:39:44 +08:00
EUForest b4893b7acc 修复 bash 脚本语法错误
- 移除 startsWith() 函数在 bash 中的使用
- 使用 github.ref_type 检测标签推送
2025-11-24 18:25:35 +08:00
EUForest adc4a4ff2c 修复 GitHub Actions 语法错误
- 合并重复的 push 触发器
- 修复 YAML 语法问题
- 确保标签推送时自动创建 Release
2025-11-24 18:23:17 +08:00
EUForest b8a4b73bb0 修复标签推送自动创建 Release
- 添加 tags 触发器支持 v* 格式的标签
- 标签推送时自动创建 GitHub Release
- 自动上传二进制文件到 Release
2025-11-24 18:21:56 +08:00
EUForest e062dc1ab0 添加手动发布功能支持
- 支持手动触发时创建 GitHub Release
- 可以为现有版本重新发布二进制文件
2025-11-24 18:19:52 +08:00
EUForest 0d57e7eae3 简化为纯 Golang Linux 二进制构建工作流
- 移除复杂的部署逻辑和 Docker 支持
- 专注于构建 Linux AMD64 二进制文件
- 自动生成 SHA256 校验和
- 支持 GitHub Release 自动上传
- 提供简洁的构建摘要
2025-11-24 18:12:12 +08:00
EUForest 5d18c7bb7d 添加 GitHub 自动部署 Linux 二进制工作流
- 新增 .github/workflows/deploy-linux.yml 支持自动构建和部署
- 添加 .goreleaser.yml 配置多平台发布
- 支持 build-only/deploy-staging/deploy-production 模式
- 包含 Docker 镜像自动构建
2025-11-24 18:10:00 +08:00
EUForest f0f29deef1 add: github workflows 2025-11-24 18:01:23 +08:00
EUForest 22d03a100a add: github workflows 2025-11-24 17:53:16 +08:00
EUForest e81a11cd59 remove: github workflows 2025-11-24 17:35:03 +08:00
Tension b29e5c8cb5 fix(ppanel): remove deprecated configuration settings from ppanel.yaml 2025-11-24 16:43:23 +08:00
EUForest 128791f43e update: bind device 2025-11-24 15:59:45 +08:00
EUForest d22659ff04 update: encryption 2025-11-24 15:59:03 +08:00
Tension c4166cef6b feat(ip-location): implement IP location querying and GeoIP database management 2025-11-23 22:38:55 +08:00
EUForest 95ddba2332 update: Added support for floating-point calculations for discounts 2025-11-21 17:40:03 +08:00
EUForest c1efb23354 update: Added support for floating-point calculations for discounts 2025-11-21 17:39:57 +08:00
Chang lue Tsen e3999ba75f feat(subscribe): add short token generation and validation logic 2025-11-18 12:03:14 -05:00
Chang lue Tsen 8a4cfcbdb3 feat(subscribe): add endpoint to reset all subscribe tokens 2025-11-18 08:37:57 -05:00
EUForest ff2d3f85f3 new features: Based on IP user registration restrictions 2025-11-13 14:52:02 +08:00
EUForest 0fb92f380f update: Remove accounts without verification methods 2025-11-13 14:50:55 +08:00
EUForest 3635d3e224 update: device delete current user 2025-11-13 14:49:25 +08:00
Chang lue Tsen c8b3683cf2 feat(report): add heartbeat URL to registration request and response 2025-11-09 10:56:55 -05:00
Chang lue Tsen 1172ecc2f1 feat(heartbeat): add heartbeat endpoint and logic for service health check 2025-11-09 10:49:51 -05:00
Chang lue Tsen 750a33cca2 feat(user): add UpdateUserSubscribeNote handler and endpoint for updating user subscription notes 2025-11-09 09:16:00 -05:00
Chang lue Tsen d0aad68bb0 style 2025-11-09 09:07:11 -05:00
Chang lue Tsen cb5bf5aae3 feat(module): add GetModuleConfig handler and logic for module configuration retrieval 2025-11-09 09:06:42 -05:00
Chang lue Tsen 87b743a2a2 refactor(constants): update repository and service name constants in tool and version files 2025-11-09 08:54:50 -05:00
Chang lue Tsen 2305562a7c feat(server): implement gateway mode handling and dynamic port registration 2025-11-09 08:46:53 -05:00
EUForest 60d584a052 add: User cancels account 2025-11-09 17:27:24 +08:00
EUForest b9d3446407 Bug fix: Clear server user node cache 2025-11-09 17:18:25 +08:00
EUForest 902608b2e0 Add: Get expired subscription 2025-11-08 17:06:30 +08:00
EUForest d7aa9a44b7 Add: Try to get the device ID from the request parameters 2025-11-08 17:05:52 +08:00
EUForest b7eafd0892 Update: Account display priority: phone number > email > device ID 2025-11-08 17:05:05 +08:00
EUForest d1a8662095 Add: Add a WebSocket connection to monitor the app's online status. 2025-11-06 15:35:49 +08:00
EUForest 8cce9b95b4 Add: Add a WebSocket connection to monitor the app's online status. 2025-11-06 15:34:51 +08:00
EUForest 066f5d6538 Update: Successful authentication records the identifier. 2025-11-06 15:33:14 +08:00
EUForest de6661cc16 Update: Successful authentication records the identifier. 2025-11-06 15:32:29 +08:00
EUForest 52ce054b35 Add: Server location latitude and longitude 2025-11-04 14:53:43 +08:00
Chang lue Tsen eb25095623 feat(report): implement module registration and gateway mode handling 2025-11-03 14:12:27 -05:00
Ember Moth a9c98b67f1 add missing proxy field mappings 2025-11-04 00:43:36 +08:00
EUForest 2fd119d697 add missing proxy field mappings 2025-11-02 16:38:19 +08:00
Tension 2ed379d5e8 feat(report): add module registration and port management functionality 2025-11-01 16:05:56 +08:00
EUForest 2605d22f8e Add: ClearServerAllCache Func 2025-11-01 16:05:37 +08:00
EUForest 829d5f3ffd fix bug: Set server zone 2025-11-01 15:57:30 +08:00
EUForest 7b8e71ade2 fix bug: SMS recharge password 2025-11-01 15:52:05 +08:00
EUForest aaea4183c2 fix bug: Get area information 2025-10-31 23:23:58 +08:00
lain d8e2e81688 add user subscribe note 2025-10-31 10:00:31 +08:00
EUForest e0003ea074 Temporary: Cancel node cache 2025-10-28 17:48:38 +08:00
EUForest 4312e20a5c fix bug: init sql fail 2025-10-28 14:24:06 +08:00
EUForest 2edc0ef1c8 fix bug: device login fail 2025-10-28 14:18:20 +08:00
EUForest b974b9a56b Merge remote-tracking branch 'upstream' 2025-10-23 12:23:08 +08:00
EUForest 9ab63dff88 fix bug: Login failed for old user 2025-10-23 12:21:43 +08:00
Chang lue Tsen a5abbc1dd3 fix(server): add unauthorized access check for secret key 2025-10-22 10:38:21 -04:00
Chang lue Tsen 528ed63a55 fix(version): log migration duration for database changes 2025-10-22 10:38:11 -04:00
EUForest b2045a6e1b Merge remote-tracking branch 'upstream' 2025-10-22 20:36:42 +08:00
EUForest cd3b9d4fc8 Merge remote-tracking branch 'upstream'
# Conflicts:
#	apis/public/subscribe.api
#	initialize/migrate/database/02115_ads.up.sql
#	internal/logic/auth/deviceLoginLogic.go
#	internal/logic/public/subscribe/queryUserSubscribeNodeListLogic.go
#	internal/logic/public/user/unbindDeviceLogic.go
#	internal/types/types.go
#	pkg/tool/encryption_test.go
2025-10-22 20:16:03 +08:00
EUForest b099331302 add: Add protocol configuration in the return node 2025-10-22 19:54:56 +08:00
EUForest d78d79fa2d add: Automatically delete token when device is removed 2025-10-22 19:52:52 +08:00
Chang lue Tsen 81748abce4 fix(traffic_log): update deletion condition to use timestamp instead of created_at 2025-10-20 10:18:07 -04:00
Chang lue Tsen aa5e472184 feat(traffic_log): add index on timestamp column and create down migration 2025-10-20 10:17:56 -04:00
Chang lue Tsen a15e08d8f3 fix(ads): add description column only if it does not already exist 2025-10-18 17:42:02 +09:00
Chang lue Tsen 6db4c1b826 fix(ads): add description column only if it does not already exist 2025-10-18 16:29:09 +09:00
Chang lue Tsen 396862f59d fix(user): modify SQL migration scripts to add columns only if they do not exist 2025-10-17 11:20:16 -04:00
Chang lue Tsen 2dae837323 fix(user): update SQL migration scripts to use IF NOT EXISTS for column additions 2025-10-17 10:20:12 -04:00
Chang lue Tsen 1b76794251 fix(coupon): add expiration check and error handling for expired coupons 2025-10-16 09:31:33 -04:00
Ember Moth 8b48286365 add payment type field (alipay/wxpay) to EPay config 2025-10-15 23:20:33 +09:00
Chang lue Tsen 643d234a88 fix(docker): improve build command timestamp format for clarity 2025-10-15 10:19:16 -04:00
Chang lue Tsen eacf675671 fix(docker): update build command for improved timestamp format and readability 2025-10-15 10:13:33 -04:00
EUForest 96808d531a Application device interface encryption and other bug fixes (#87)
* add: device login

* update: global config

* add: User transmission interface encryption

* update: get global config

* update: User transmission interface encryption

* add: get device list

* add: SecretIsEmpty Message

* update: device middleware

* add: query user subscribe node list

* fix bug: query device list

* fix bug: unbind device

* update: device login

* fix bug: The ad table is missing the description field

* fix bug:page size is zero

* update: Device Middleware

* fix bug: Site custom data update failed
2025-10-15 10:09:19 -04:00
Chang lue Tsen adbe9a06d8 feat(exchange): add exchange rate handling and scheduled updates 2025-10-15 10:05:19 -04:00
EUForest fb226a0fc1 fix bug: Site custom data update failed 2025-10-14 15:57:39 +08:00
EUForest 571628710b update: Device Middleware 2025-10-14 15:08:59 +08:00
EUForest f67c2e34dc fix bug:page size is zero 2025-10-14 15:07:35 +08:00
EUForest 15beff410b fix bug: The ad table is missing the description field 2025-10-14 11:54:32 +08:00
EUForest f0e2633ef6 update: device login 2025-10-14 11:37:25 +08:00
EUForest b80c7caddd fix bug: unbind device 2025-10-14 11:37:13 +08:00
EUForest 37ad4c8443 fix bug: query device list 2025-10-13 19:22:52 +08:00
EUForest 640b8c0805 add: query user subscribe node list 2025-10-13 19:16:36 +08:00
EUForest e99058b969 update: device middleware 2025-10-13 19:16:05 +08:00
EUForest 39306f3043 add: SecretIsEmpty Message 2025-10-13 19:15:38 +08:00
EUForest 9ea5c626e9 add: get device list 2025-10-12 19:11:39 +08:00
EUForest 8c776cdbac update: User transmission interface encryption 2025-10-12 19:10:52 +08:00
EUForest 3efa68d3ff update: get global config 2025-10-12 17:36:30 +08:00
EUForest 46e6a9784d add: User transmission interface encryption 2025-10-12 16:23:29 +08:00
EUForest f3bc933a99 update: global config 2025-10-12 15:23:58 +08:00
EUForest 71018eb2f4 add: device login 2025-10-11 15:55:45 +08:00
Chang lue Tsen 8562734fde feat(user): add encryption algorithm and salt fields to User model for migrate 2025-10-09 13:33:03 -04:00
Chang lue Tsen de4386ff68 feat(encryption): add MultiPasswordVerify function for multiple hashing algorithms 2025-10-08 12:19:41 -04:00
Chang lue Tsen a273f8d883 fix(config): update protocol handling for Hysteria2 compatibility 2025-10-07 09:51:27 -04:00
Chang lue Tsen 6dcd75a39b fix(config): compatible hysteria2, remove in future versions 2025-10-04 12:00:42 -04:00
Chang lue Tsen 29dd54546d fix(model): enhance InSet function for multi-value OR queries 2025-10-04 11:51:47 -04:00
Chang lue Tsen 41d8de42c0 feat(constant): deprecate Hysteria2 in favor of Hysteria 2025-10-04 05:10:04 -04:00
Chang lue Tsen b97e2d650e refactor(cache): streamline subscription cache clearing logic 2025-10-03 15:20:11 -04:00
Chang lue Tsen c5be0081ca fix(model): simplify tag filtering using GORM scopes 2025-10-03 14:53:52 -04:00
Chang lue Tsen 1a6cced814 fix(docs): update Docker pull and run commands to use official ppanel image 2025-10-02 18:27:21 +09:00
Chang lue Tsen bf24e1756a fix(checkSubscriptionLogic): update status filter to include inactive subscriptions 2025-10-02 17:41:42 +09:00
Chang lue Tsen 4e7a6e9e41 fix(previewSubscribeTemplateLogic): add enabled filter to node list query 2025-10-02 16:51:32 +09:00
Chang lue Tsen 6c7d45a640 fix(subscribeLogic): correct protocol JSON formatting and add server ID 2025-10-02 16:51:24 +09:00
Chang lue Tsen c835e751ac feat(node): update Hysteria protocol to use new naming convention 2025-09-29 11:30:23 -04:00
Chang lue Tsen 3fb23e3106 refactor(adapter): delete old adapter 2025-09-29 11:22:36 -04:00
Chang lue Tsen 766e210f06 fix(node): update FilterNodeParams to use NodeId and adjust query logic for tags 2025-09-29 11:18:47 -04:00
Chang lue Tsen 2de76104ad fix(tests): update time format in TestNewNodeMultiplierManager and add new time period 2025-09-29 15:18:58 +09:00
Chang lue Tsen 453ad18303 feat(node): add PreViewNodeMultiplier endpoint and response structure 2025-09-29 13:12:37 +09:00
Chang lue Tsen 02f0d711ba fix(manager): update time parsing format in isInTimePeriod function 2025-09-29 13:11:51 +09:00
Chang lue Tsen d6fcff1fee fix(node): NodeId filter to FilterNodeParams and update cache clearing logic 2025-09-29 10:22:11 +09:00
Ember Moth 25c7eb3927 fix(purchase): resolve balance payment transaction inconsistency causing Internal Server Error (#81) 2025-09-28 12:34:18 -04:00
Chang lue Tsen 9777a0c61a fix(purchase): update payment processing to use CryptoSaaSPayment method 2025-09-28 12:28:44 -04:00
Chang lue Tsen d74d636735 feat(traffic): add protocol handling and update traffic statistics calculation 2025-09-28 11:31:21 -04:00
Chang lue Tsen 5aa9bb61b7 refactor(server): remove Ratio field from Server struct and update adapter logic 2025-09-28 11:18:33 -04:00
Chang lue Tsen 42a31048b4 fix(subscribe): ignore subscription expiration check in Generate Subscribe 2025-09-28 11:14:54 -04:00
Chang lue Tsen 0179e9837c refactor(api): remove Ratio field from NodeConfig and related structures 2025-09-28 11:14:43 -04:00
Chang lue Tsen 0d1df5f905 feat(config): enhance NodeConfig with TrafficReportThreshold, IPStrategy, DNS, Block, and Outbound fields 2025-09-28 19:06:29 +09:00
Chang lue Tsen 64f0bcc55a feat(cache): implement cache clearing for node operations and add delete methods 2025-09-27 11:24:29 -04:00
Chang lue Tsen d2ca3eb961 fix(subscribe): add Enabled filter to node query parameters 2025-09-27 10:18:08 -04:00
Chang lue Tsen c1c324878b fix(traffic): skip logs with zero upload and download values 2025-09-27 10:16:28 -04:00
Chang lue Tsen 5ca4179285 feat(user): add OnlyFirstPurchase field to user creation logic 2025-09-27 10:13:41 -04:00
Chang lue Tsen 9ce6141b5a fix(log): correct model reference in SystemLog creation 2025-09-27 10:11:58 -04:00
Chang lue Tsen 56352de870 chore(deploy): comment out server deployment steps in workflow 2025-09-27 10:11:10 -04:00
Chang lue Tsen 4ad18a8b70 fix(model): update save method to use specific ID for record updates 2025-09-16 11:20:48 -04:00
Chang lue Tsen 596434454c feat(client): add additional options for Shadowsocks and Vless protocols 2025-09-16 11:20:39 -04:00
Chang lue Tsen b5c756af35 feat(subscribe): update subscribe path 2025-09-16 10:39:19 -04:00
Chang lue Tsen 621eda41c2 fix(client): Shadowrocket template 2025-09-16 10:26:39 -04:00
Chang lue Tsen 900096ed12 fix(coupon): add check for zero count in coupon usage validation 2025-09-16 10:17:18 -04:00
Chang lue Tsen 26fe430d7c fix(subscribe): update status filter in subscription count query 2025-09-16 10:16:56 -04:00
Chang lue Tsen 0c55d173cb fix(model): add sorting to server and node queries 2025-09-16 10:10:11 -04:00
Chang lue Tsen 537dab1c3b fix(quota): simplify gift amount calculation 2025-09-16 09:53:03 -04:00
Leif Draven 4b2976a670 fix(schema): remove unnecessary AUTO_INCREMENT values from initial schema (#79)
Co-authored-by: Chang lue Tsen <tension@ppanel.dev>
2025-09-16 09:33:28 -04:00
Chang lue Tsen 38491ddc0d Merge branch 'master' into develop 2025-09-14 11:34:07 -04:00
Leif Draven e895180388 Develop (#76)
* refactor: rename queryannouncementhandler.go to queryAnnouncementLogic.go for clarity

* feat(panDomain): update subscription logic to use V2 handler for improved functionality

* refactor(subscribe): replace V2 handler with a unified Handler method for subscription logic

* feat(subscribe): implement user agent limit feature with configurable list

* fix(subscribe): improve error handling and logging for subscription requests

* feat(subscribe): add user agent limit configuration to system settings

* refactor(api): remove deprecated application-related endpoints and types

* refactor(swagger): remove deprecated app.json generation from swagger configuration

* refactor(swagger): remove deprecated app.json check from swagger configuration

* fix(subscribe): update delete method to use Where clause for improved query accuracy

* fix(subscribe): update Id field tag to use primaryKey and improve save method query

* fix(subscribe): update Id field tag to use primaryKey and improve model queries

* fix(subscribe): rename variable for clarity and add special handling for Stash user agent

* fix(email): convert RegisterStartTime and RegisterEndTime to time.Time for accurate query filtering

* refactor(log): consolidate logging models and update related logic for improved clarity and functionality

* fix(types): change Content field type in MessageLog to interface{} for improved flexibility

* fix(log): change MessageLog list to use value type for improved performance and memory efficiency

* fix(email): set EmailTypeVerify in task payload and update content type conversion for verification email

* fix(log): remove unused Id field from SystemLog during login log insertion

* fix(login): remove debug logs and error logging during user login process

* fix(log): add traffic reset logging for subscription resets

* fix(log): insert reset traffic log during subscription activation

* feat(log): add endpoints for retrieving and resetting subscribe traffic logs

* refactor(log): remove Reset Subscribe Traffic Log endpoint and related types

* feat(traffic): add traffic statistics logging and scheduling

* fix(subscribe): ensure active status and reset timestamps during traffic resets

* feat(api): enhance server and node management with new request/response structures

* refactor(api): rename OnlineUser to ServerOnlineUser for clarity

* feat(api): define OnlineUser type with SID and IP fields

* feat(server): implement server management handlers and database schema

* feat(api): add traffic log details filtering and enhance traffic log structures

* feat(api): migrate server and node data handling, update related structures and logic

* feat(server): implement server deletion logic with error handling

* feat(api): update log filtering to use ResetSubscribe type for subscription logs

* feat(api): standardize timestamp field across log structures

* feat(api): refactor cache key handling for server and user lists

* feat(api): enhance server status handling with protocol support and refactor related logic

* fix(traffic): adjust start date for traffic statistics and improve log deletion comment

* feat(api): implement daily traffic ranking for users and servers with error handling

* feat(api): update server total data response to use 'OnlineUsers' and implement daily traffic statistics logging

* feat(api): add log settings management with auto-clear and clear days configuration

* fix(log): correct category in log settings update query

* feat(routes): add handler for scheduled traffic statistics

* feat(model): add user counts struct and update queries for new and renewal users

* feat(api): add referral percentage and only first purchase fields to user model and requests

* feat(database): update user table to add referral percentage and only first purchase fields

* feat(api): add reset sort endpoints for server and node

* feat(api): add sort field to server model

* feat(api): implement sorting functionality for nodes and servers

* fix(database): add sort column to nodes table

* fix(model): enhance user statistics queries with new order and renewal order counts

* fix(log): update timestamp handling in login and registration logs

* fix(log): update sorting logic for server and user subscribe traffic logs

* fix(server): add server status handling based on last reported time

* fix(model): correct filter condition to use 'date' instead of 'data'

* fix(migration): add index for traffic log on timestamp, user_id, and subscribe_id

* fix(log): optimize user traffic rank data handling by using append instead of index assignment

* fix(filter): refactor node list creation to use append and remove duplicates from tags

* fix(node): add ServerId and Enabled fields to node update logic

* feat(tags): add endpoint to query all node tags

* fix(preview): add Preload parameter to FilterNodeList for improved data retrieval

* fix(log): date is empty

* feat(subscribe): add Language field to subscription models and update query logic

* feat(subscription): add Language parameter to GetSubscription request and update query logic

* fix(server): encode ServerKey in base64 and update last reported time for nodes

* feat: delete common GetSubscription

* feat(subscription): implement FilterList method for subscription queries and update related logic

* fix(subscribe): remove duplicate user agents in SubscribeHandler

* fix(push): initialize onlineUsers as a map in pushOnlineUsersLogic

* fix(reset): initialize subs as a map in clearCache method

* refactor(query): simplify node and tag filtering using InSet function

* feat(userlist): enhance GetServerUserListLogic with improved node and tag handling

* fix(userlist): correct node ID assignment and update query logic for tag filtering

* fix(userlist): correct node ID assignment in getServerUserListLogic

* refactor(query): streamline query construction for tag filtering

* fix(statistics): optimize server ranking data handling in QueryServerTotalDataLogic

* refactor(statistics): simplify server ranking data construction in QueryServerTotalDataLogic

* fix(statistics): correct server traffic data assignment in QueryServerTotalDataLogic

* fix(statistics): optimize yesterday's top 10 server traffic data assignment in QueryServerTotalDataLogic

* fix(middleware): remove duplicate elements from user agent list in PanDomainMiddleware

* feat(middleware): enhance user agent handling by querying client list in PanDomainMiddleware

* feat(client): subscribe_template

* feat(oauth): add user agent and IP logging to registration and login processes

* fix(balance): add timestamp to balance logs for payment, refund, and recharge transactions

* fix(log): correct comment for CommissionTypeRefund to improve clarity

* fix(log): replace magic number with constant for gift type in purchase checkout logic

* fix(log): rename OrderId to OrderNo for consistency in balance logging

* feat(log): add logging for balance, gift amount, and commission adjustments

* fix(user): correct placement of DeepCopy for user info update logic

* feat(log): add UserSubscribeId to FilterSubscribeLogRequest for enhanced filtering

* fix(purchase): streamline error handling and improve JSON marshaling for temporary orders

* fix(order): simplify commission handling and improve payload parsing logic

* fix(order): update commission calculation to actual payment amount minus gateway handling fee

* feat(payment): add support for CryptoSaaS payment platform and enhance configuration handling

* fix(balance): update QueryUserBalanceLog response structure to include balance log list

* fix(email): update task progress handling to use specific task ID for updates

* feat(quota): add quota task creation and querying endpoints with updated data structures

* fix(email): update task handling to use generic task model and improve error logging

* fix(order): improve error logging for database transaction and user cache updates

* feat(quota): enhance quota task management with new request structures and processing logic

* fix(quota): remove redundant quota task status endpoint from admin marketing routes

* fix(worker): update task completion status handling in worker logic

* fix(quota): update taskInfo to include current subscription count in quota logic

* doc(log): rename function for clarity and add cache cleanup comment

* fix(quota): update time handling in quota logic and correct subscriber ID query

* fix(quota): update time handling to use UnixMilli for start time in quota logic

* feat(protocol): add server protocol configuration query and enhance protocol options

* fix(quota): correct time range queries for start and expire times in quota logic

* fix(types): update plugin options to include 'none' in the plugin field

---------

Co-authored-by: Chang lue Tsen <tension@ppanel.dev>
2025-09-14 09:50:22 -04:00
Chang lue Tsen 435283f962 fix(types): update plugin options to include 'none' in the plugin field 2025-09-14 09:46:14 -04:00
Chang lue Tsen b0ffb15825 fix(quota): correct time range queries for start and expire times in quota logic 2025-09-14 09:43:21 -04:00
Chang lue Tsen 2c5efa2026 feat(protocol): add server protocol configuration query and enhance protocol options 2025-09-13 14:20:04 -04:00
Chang lue Tsen c1c913cb00 fix(quota): update time handling to use UnixMilli for start time in quota logic 2025-09-13 14:12:33 -04:00
Chang lue Tsen cf4792cdc8 fix(quota): update time handling in quota logic and correct subscriber ID query 2025-09-13 14:02:49 -04:00
Chang lue Tsen 9b0e1b1e0f doc(log): rename function for clarity and add cache cleanup comment 2025-09-11 11:42:20 -04:00
Chang lue Tsen 6bc0fb4d90 fix(quota): update taskInfo to include current subscription count in quota logic 2025-09-11 06:29:12 -04:00
Chang lue Tsen 32633920b5 fix(worker): update task completion status handling in worker logic 2025-09-11 06:24:52 -04:00
Chang lue Tsen d55bad712c fix(quota): remove redundant quota task status endpoint from admin marketing routes 2025-09-10 14:54:31 -04:00
Chang lue Tsen 3f5aac239b feat(quota): enhance quota task management with new request structures and processing logic 2025-09-10 14:53:48 -04:00
Chang lue Tsen 83c1c14b01 fix(order): improve error logging for database transaction and user cache updates 2025-09-10 10:41:29 -04:00
Chang lue Tsen 1c1365d862 fix(email): update task handling to use generic task model and improve error logging 2025-09-10 09:02:54 -04:00
Chang lue Tsen d1be5febc3 feat(quota): add quota task creation and querying endpoints with updated data structures 2025-09-09 13:39:05 -04:00
Chang lue Tsen f4c6bd919b fix(email): update task progress handling to use specific task ID for updates 2025-09-08 12:53:28 -04:00
Chang lue Tsen 31cdcd2cb9 fix(balance): update QueryUserBalanceLog response structure to include balance log list 2025-09-07 11:59:34 -04:00
Chang lue Tsen 4d95834c22 feat(payment): add support for CryptoSaaS payment platform and enhance configuration handling 2025-09-06 12:22:39 -04:00
Chang lue Tsen 47446ef410 fix(order): update commission calculation to actual payment amount minus gateway handling fee 2025-09-06 11:41:17 -04:00
Chang lue Tsen 0872099e98 fix(order): simplify commission handling and improve payload parsing logic 2025-09-06 11:28:32 -04:00
Chang lue Tsen d5ed82955e fix(purchase): streamline error handling and improve JSON marshaling for temporary orders 2025-09-06 11:03:14 -04:00
Chang lue Tsen d477ba4772 feat(log): add UserSubscribeId to FilterSubscribeLogRequest for enhanced filtering 2025-09-05 13:35:55 -04:00
Chang lue Tsen 6460f2f2e1 fix(user): correct placement of DeepCopy for user info update logic 2025-09-05 11:14:38 -04:00
Chang lue Tsen 0199dc7fa2 feat(log): add logging for balance, gift amount, and commission adjustments 2025-09-05 11:00:19 -04:00
Chang lue Tsen 1d878592ed fix(log): rename OrderId to OrderNo for consistency in balance logging 2025-09-05 10:25:19 -04:00
Chang lue Tsen d0d03e724e fix(log): replace magic number with constant for gift type in purchase checkout logic 2025-09-05 09:52:45 -04:00
Chang lue Tsen 2f4cc46a38 fix(log): correct comment for CommissionTypeRefund to improve clarity 2025-09-05 09:51:28 -04:00
Chang lue Tsen 3199dc5141 fix(balance): add timestamp to balance logs for payment, refund, and recharge transactions 2025-09-05 09:29:19 -04:00
Chang lue Tsen 71d32d73bb feat(oauth): add user agent and IP logging to registration and login processes 2025-09-05 09:26:10 -04:00
Chang lue Tsen 584067375b feat(client): subscribe_template 2025-09-05 05:13:30 -04:00
Chang lue Tsen b9ef8f4572 feat(middleware): enhance user agent handling by querying client list in PanDomainMiddleware 2025-09-05 04:28:05 -04:00
Chang lue Tsen f3c73a5224 fix(middleware): remove duplicate elements from user agent list in PanDomainMiddleware 2025-09-05 04:16:33 -04:00
Chang lue Tsen 51335f4c19 fix(statistics): optimize yesterday's top 10 server traffic data assignment in QueryServerTotalDataLogic 2025-09-04 15:31:19 -04:00
Chang lue Tsen 7912d2908f fix(statistics): correct server traffic data assignment in QueryServerTotalDataLogic 2025-09-04 15:26:52 -04:00
Chang lue Tsen e913f974bb refactor(statistics): simplify server ranking data construction in QueryServerTotalDataLogic 2025-09-04 15:21:07 -04:00
Chang lue Tsen e8d3ebba68 fix(statistics): optimize server ranking data handling in QueryServerTotalDataLogic 2025-09-04 15:11:12 -04:00
Chang lue Tsen b4a05166a4 refactor(query): streamline query construction for tag filtering 2025-09-04 14:17:26 -04:00
Chang lue Tsen 712e31cc60 fix(userlist): correct node ID assignment in getServerUserListLogic 2025-09-04 14:09:17 -04:00
Chang lue Tsen 60988bc2ba fix(userlist): correct node ID assignment and update query logic for tag filtering 2025-09-04 14:05:04 -04:00
Chang lue Tsen 61ac829c37 feat(userlist): enhance GetServerUserListLogic with improved node and tag handling 2025-09-04 13:46:24 -04:00
Chang lue Tsen 805fca90e0 refactor(query): simplify node and tag filtering using InSet function 2025-09-04 13:08:57 -04:00
Chang lue Tsen ef7491d025 fix(reset): initialize subs as a map in clearCache method 2025-09-04 12:32:28 -04:00
Chang lue Tsen a2d8d7d0bd fix(push): initialize onlineUsers as a map in pushOnlineUsersLogic 2025-09-04 12:13:55 -04:00
Chang lue Tsen e798f8de1b fix(subscribe): remove duplicate user agents in SubscribeHandler 2025-09-04 11:38:46 -04:00
Chang lue Tsen 0636a4bddf feat(subscription): implement FilterList method for subscription queries and update related logic 2025-09-04 10:20:35 -04:00
Chang lue Tsen 8f11380e7a feat: delete common GetSubscription 2025-09-04 09:32:39 -04:00
Chang lue Tsen 7ecf955eb7 fix(server): encode ServerKey in base64 and update last reported time for nodes 2025-09-04 09:26:50 -04:00
Chang lue Tsen 10757612f5 feat(subscription): add Language parameter to GetSubscription request and update query logic 2025-09-04 03:14:31 -04:00
Chang lue Tsen f632ea2c89 feat(subscribe): add Language field to subscription models and update query logic 2025-09-03 15:44:04 -04:00
Chang lue Tsen f8e6cf515e fix(log): date is empty 2025-09-03 12:35:32 -04:00
Chang lue Tsen 73ffd0d1c7 fix(preview): add Preload parameter to FilterNodeList for improved data retrieval 2025-09-03 12:09:14 -04:00
Chang lue Tsen 1f824fd5a9 feat(tags): add endpoint to query all node tags 2025-09-03 11:57:12 -04:00
Chang lue Tsen 42df466104 fix(node): add ServerId and Enabled fields to node update logic 2025-09-03 09:49:57 -04:00
Chang lue Tsen 68000574b2 fix(filter): refactor node list creation to use append and remove duplicates from tags 2025-09-03 09:42:42 -04:00
Chang lue Tsen 64366d0dec fix(log): optimize user traffic rank data handling by using append instead of index assignment 2025-09-03 09:20:26 -04:00
Chang lue Tsen 949efdad34 fix(migration): add index for traffic log on timestamp, user_id, and subscribe_id 2025-09-03 07:03:37 -04:00
Chang lue Tsen 107fe82f10 fix(model): correct filter condition to use 'date' instead of 'data' 2025-09-03 06:49:55 -04:00
Chang lue Tsen ca344a1ce9 fix(server): add server status handling based on last reported time 2025-09-02 14:38:53 -04:00
Chang lue Tsen 3e5284c4ec fix(log): update sorting logic for server and user subscribe traffic logs 2025-09-02 14:29:50 -04:00
Chang lue Tsen fe629e59dd fix(log): update timestamp handling in login and registration logs 2025-09-02 14:12:06 -04:00
Chang lue Tsen d578dca17e fix(model): enhance user statistics queries with new order and renewal order counts 2025-09-02 14:06:26 -04:00
Chang lue Tsen ff574b3955 fix(database): add sort column to nodes table 2025-09-02 11:51:07 -04:00
Chang lue Tsen 26b2afe06d feat(api): implement sorting functionality for nodes and servers 2025-09-01 13:13:07 -04:00
Chang lue Tsen d4850a73f3 feat(api): add sort field to server model 2025-09-01 12:24:15 -04:00
Chang lue Tsen 367ef9d2e7 feat(api): add reset sort endpoints for server and node 2025-09-01 11:41:34 -04:00
Chang lue Tsen e910d0e345 feat(database): update user table to add referral percentage and only first purchase fields 2025-09-01 09:40:33 -04:00
Chang lue Tsen aea20ffd5e feat(api): add referral percentage and only first purchase fields to user model and requests 2025-09-01 09:03:39 -04:00
Chang lue Tsen 4438b05272 feat(model): add user counts struct and update queries for new and renewal users 2025-09-01 08:25:53 -04:00
Chang lue Tsen 1745c7194b feat(routes): add handler for scheduled traffic statistics 2025-09-01 08:14:57 -04:00
Chang lue Tsen 4432115844 fix(log): correct category in log settings update query 2025-09-01 06:31:41 -04:00
Chang lue Tsen b6cae7bbb5 feat(api): add log settings management with auto-clear and clear days configuration 2025-09-01 06:11:35 -04:00
Chang lue Tsen 4184a32c0f feat(api): update server total data response to use 'OnlineUsers' and implement daily traffic statistics logging 2025-09-01 05:30:05 -04:00
Chang lue Tsen 9ff965bbea feat(api): implement daily traffic ranking for users and servers with error handling 2025-09-01 05:03:02 -04:00
Chang lue Tsen 8a37472e61 fix(traffic): adjust start date for traffic statistics and improve log deletion comment 2025-09-01 05:02:53 -04:00
Chang lue Tsen 3012a68339 feat(api): enhance server status handling with protocol support and refactor related logic 2025-09-01 04:17:33 -04:00
Chang lue Tsen 6dc1bee14c feat(api): refactor cache key handling for server and user lists 2025-08-31 11:29:55 -04:00
Chang lue Tsen 82fd674ae4 feat(api): standardize timestamp field across log structures 2025-08-26 14:39:54 -04:00
Chang lue Tsen 88cbfbb7d6 feat(api): update log filtering to use ResetSubscribe type for subscription logs 2025-08-26 14:26:55 -04:00
Chang lue Tsen 92466d62a4 feat(server): implement server deletion logic with error handling 2025-08-26 10:49:03 -04:00
Chang lue Tsen c7884d94aa feat(api): migrate server and node data handling, update related structures and logic 2025-08-26 07:05:59 -04:00
Chang lue Tsen 9b3cdbbb4f feat(api): add traffic log details filtering and enhance traffic log structures 2025-08-26 02:29:25 -04:00
Chang lue Tsen ad4f3df74e feat(server): implement server management handlers and database schema 2025-08-25 14:06:37 -04:00
lyndon986 b5e244d83d renew readme_zh.md 2025-08-26 01:57:50 +10:00
lyndon986 2acbdbc9e0 renew README.md 2025-08-26 01:56:34 +10:00
Chang lue Tsen 1ccbdc18b1 feat(api): define OnlineUser type with SID and IP fields 2025-08-25 11:16:39 -04:00
Chang lue Tsen 630098bfbb refactor(api): rename OnlineUser to ServerOnlineUser for clarity 2025-08-25 11:16:25 -04:00
Chang lue Tsen 7bd026a600 feat(api): enhance server and node management with new request/response structures 2025-08-25 10:10:13 -04:00
Chang lue Tsen d5460d0cd1 fix(subscribe): ensure active status and reset timestamps during traffic resets 2025-08-23 09:07:56 -04:00
Chang lue Tsen d33f4cd1ce feat(traffic): add traffic statistics logging and scheduling 2025-08-21 14:11:03 -04:00
Chang lue Tsen 6b1b365734 refactor(log): remove Reset Subscribe Traffic Log endpoint and related types 2025-08-21 11:19:45 -04:00
Chang lue Tsen 062533412a feat(log): add endpoints for retrieving and resetting subscribe traffic logs 2025-08-21 11:02:11 -04:00
Chang lue Tsen ec4e95451b fix(log): insert reset traffic log during subscription activation 2025-08-21 10:53:12 -04:00
Chang lue Tsen fcdb9f72ef fix(log): add traffic reset logging for subscription resets 2025-08-21 10:35:40 -04:00
Chang lue Tsen 7c707f2ecf fix(login): remove debug logs and error logging during user login process 2025-08-21 09:18:14 -04:00
Chang lue Tsen 70815c0d50 fix(log): remove unused Id field from SystemLog during login log insertion 2025-08-21 09:15:37 -04:00
Chang lue Tsen 38706c0b29 fix(email): set EmailTypeVerify in task payload and update content type conversion for verification email 2025-08-21 09:07:48 -04:00
Chang lue Tsen 5e903868c4 fix(log): change MessageLog list to use value type for improved performance and memory efficiency 2025-08-21 08:49:13 -04:00
Chang lue Tsen c04923015e fix(types): change Content field type in MessageLog to interface{} for improved flexibility 2025-08-21 08:24:30 -04:00
Chang lue Tsen 87c771bbd4 refactor(log): consolidate logging models and update related logic for improved clarity and functionality 2025-08-20 13:36:06 -04:00
Chang lue Tsen 4f32d67113 fix(email): convert RegisterStartTime and RegisterEndTime to time.Time for accurate query filtering 2025-08-20 08:22:48 -04:00
Chang lue Tsen bc1e6315a8 fix(subscribe): rename variable for clarity and add special handling for Stash user agent 2025-08-18 02:54:00 -04:00
Chang lue Tsen 3a98616093 fix(subscribe): update Id field tag to use primaryKey and improve model queries 2025-08-18 02:33:17 -04:00
Chang lue Tsen 41c51fbbff fix(subscribe): update Id field tag to use primaryKey and improve save method query 2025-08-18 02:30:35 -04:00
Chang lue Tsen 3bb83edf05 fix(subscribe): update delete method to use Where clause for improved query accuracy 2025-08-18 02:09:15 -04:00
Chang lue Tsen 245f347d22 refactor(swagger): remove deprecated app.json check from swagger configuration 2025-08-17 12:39:17 -04:00
Chang lue Tsen 70a0fd96ab refactor(swagger): remove deprecated app.json generation from swagger configuration 2025-08-17 12:37:27 -04:00
Chang lue Tsen 8996a62b54 refactor(api): remove deprecated application-related endpoints and types 2025-08-17 12:14:55 -04:00
Chang lue Tsen 14a86c50b2 feat(subscribe): add user agent limit configuration to system settings 2025-08-17 11:52:14 -04:00
Chang lue Tsen 5d80d95568 fix(subscribe): improve error handling and logging for subscription requests 2025-08-17 10:01:12 -04:00
Chang lue Tsen 6580cc9d44 feat(subscribe): implement user agent limit feature with configurable list 2025-08-16 13:37:09 -04:00
Chang lue Tsen 4f2aafd8b2 refactor(subscribe): replace V2 handler with a unified Handler method for subscription logic 2025-08-16 12:59:28 -04:00
Chang lue Tsen 81a46d1f13 feat(panDomain): update subscription logic to use V2 handler for improved functionality 2025-08-16 06:18:44 -04:00
Chang lue Tsen 2dfb1030e1 refactor: rename queryannouncementhandler.go to queryAnnouncementLogic.go for clarity 2025-08-15 14:58:01 -04:00
Chang lue Tsen 2be1c4f6ed feat(subscription): enhance subscription cache management and improve error handling 2025-08-15 14:45:54 -04:00
Chang lue Tsen 740dd48763 feat(subscribe): update subscription route to use V2 handler 2025-08-15 13:05:36 -04:00
Leif Draven 41d660bb9e Develop (#64)
* fix(database): correct name entry for SingBox in initialization script

* fix(purchase): update gift amount deduction logic and handle zero-amount order status

* feat: add type and default fields to rule group requests and update related logic

* feat(rule): implement logic to set a default rule group during creation and update

* fix(rule): add type and default fields to rule group model and update related logic

* feat(proxy): enhance proxy group handling and sorting logic

* refactor(proxy): replace hardcoded group names with constants for better maintainability

* fix(proxy): update group selection logic to skip empty and default names

* feat(proxy): enhance proxy and group handling with new configuration options

* feat(surge): add Surge adapter support and enhance subscription URL handling

* feat(traffic): implement traffic reset logic for subscription cycles

* feat(auth): improve email and mobile config unmarshalling with default values

* fix(auth) upbind email not update

* fix(order) discount set default 1

* fix(order) discount set default 1

* fix: refactor surfboard proxy handling and enhance configuration template

* fix(renewal) discount set default 1

* feat(loon): add Loon configuration template and enhance proxy handling

* feat(subscription): update user subscription status based on expiration time

* fix(renewal): update subscription retrieval method to use token instead of order ID

* feat(order): enhance order processing logic with improved error handling and user subscription management

* fix(order): improve code quality and fix critical bugs in order processing logic

- Fix inconsistent logging calls across all order logic files
- Fix critical gift amount deduction logic bug in renewal process
- Fix variable shadowing errors in database transactions
- Add comprehensive Go-standard documentation comments
- Improve log prefix consistency for better debugging
- Remove redundant discount validation code

* fix(docker): add build argument for version in Docker image build process

* feat(version): add endpoint to retrieve application version information

* fix(auth): improve user authentication method logic and update user cache

* feat(user): add ordering functionality to user list retrieval

* fix(RevenueStatistics) fill list

* fix(UserStatistics) fill list

* fix(user): implement user cache clearing after auth method operations

* fix(auth): enhance OAuth login logic with improved request handling and user registration flow

* fix(user): implement sorting for authentication methods based on priority

* fix(user): correct ordering clause for user retrieval based on filter

* refactor(user): streamline cache management and enhance cache clearing logic

* feat(logs) set logs volume in develop

* fix(handler): implement browser interception to deny access for specific user agents

* fix(resetTraffic) reset daily server

* refactor(trojan): remove unused parameter and clean up logging in slice

* fix(middleware): add domain length check and improve user-agent handling

* fix(middleware): reorder domain processing and enhance user-agent handling

* fix(resetTraffic): update subscription reset logic to use expire_time for monthly and yearly checks

* fix(scheduler): update reset traffic task schedule to run daily at 00:30

* fix(traffic): enhance traffic reset logic for subscriptions and adjust status checks

* fix(activateOrder): update traffic reset logic to include reset day check

* feat(marketing): add batch email task management API and logic

* feat(application): implement CRUD operations for subscribe applications

* feat(types): add user agent limit and list to subscription configuration

* feat(application): update subscription application requests to include structured download links

* feat(application): add scheme field and download link handling to subscribe application

* feat(application): add endpoint to retrieve client information

* feat(application): move DownloadLink and SubscribeApplication types to types.api

* feat(application): add DownloadLink and SubscribeClient types, update client response structure

* feat(application): remove ProxyTemplate field from application API

* feat(application): implement adapter for client configuration and add preview template functionality

* feat(application): move DownloadLink type to types.api and remove from common.api

* feat(application): update PreviewSubscribeTemplate to return structured response

* feat(application): remove ProxyTemplate field from application API

* feat(application): enhance cache key generation for user list and server data

* feat(subscribe): add ClearCache method to manage subscription cache invalidation

* feat(payment): add Description field to PaymentMethodDetail response

* feat(subscribe): update next reset time calculation to use ExpireTime

* feat(purchase): include handling fee in total amount calculation

* feat(subscribe): add V2SubscribeHandler and logic for enhanced subscription management

* feat(subscribe): add output format configuration to subscription adapter

* feat(application): default data

---------

Co-authored-by: Chang lue Tsen <tension@ppanel.dev>
Co-authored-by: NoWay <Bob455668@hotmail.com>
2025-08-15 12:30:21 -04:00
Chang lue Tsen c8de30f78c fix(order): improve code quality and fix critical bugs in order processing logic
- Fix inconsistent logging calls across all order logic files
- Fix critical gift amount deduction logic bug in renewal process
- Fix variable shadowing errors in database transactions
- Add comprehensive Go-standard documentation comments
- Improve log prefix consistency for better debugging
- Remove redundant discount validation code
2025-07-24 03:06:44 +09:00
Chang lue Tsen 46ab012b6c feat(order): enhance order processing logic with improved error handling and user subscription management 2025-07-24 03:06:44 +09:00
Chang lue Tsen 8976724c1e fix(renewal): update subscription retrieval method to use token instead of order ID 2025-07-24 03:06:44 +09:00
Chang lue Tsen db63281250 feat(subscription): update user subscription status based on expiration time 2025-07-24 03:06:44 +09:00
Chang lue Tsen f3a8f94b4b feat(loon): add Loon configuration template and enhance proxy handling 2025-07-24 03:06:44 +09:00
NoWay f4cd567492 fix(renewal) discount set default 1 2025-07-24 03:06:44 +09:00
Chang lue Tsen c08fadb347 fix: refactor surfboard proxy handling and enhance configuration template 2025-07-24 03:06:44 +09:00
NoWay a155a4f8cd fix(order) discount set default 1 2025-07-24 03:06:44 +09:00
NoWay 1b78e04113 fix(order) discount set default 1 2025-07-24 03:06:44 +09:00
NoWay f6ce7b2cd3 fix(auth) upbind email not update 2025-07-24 03:06:44 +09:00
Chang lue Tsen 6034a32e85 feat(auth): improve email and mobile config unmarshalling with default values 2025-07-24 03:06:44 +09:00
Chang lue Tsen 0e8e2d442e feat(traffic): implement traffic reset logic for subscription cycles 2025-07-24 03:06:44 +09:00
Chang lue Tsen b83ce5090a feat(surge): add Surge adapter support and enhance subscription URL handling 2025-07-24 03:06:44 +09:00
Chang lue Tsen 224365ce79 feat(proxy): enhance proxy and group handling with new configuration options 2025-07-24 03:06:44 +09:00
Chang lue Tsen 82e447c55e fix(proxy): update group selection logic to skip empty and default names 2025-07-24 03:06:44 +09:00
Chang lue Tsen 8c4c7d0773 refactor(proxy): replace hardcoded group names with constants for better maintainability 2025-07-24 03:06:44 +09:00
Chang lue Tsen 97024dd1df feat(proxy): enhance proxy group handling and sorting logic 2025-07-24 03:06:44 +09:00
Chang lue Tsen 979e39b9e5 fix(rule): add type and default fields to rule group model and update related logic 2025-07-24 03:06:44 +09:00
Chang lue Tsen 94d316ec52 feat(rule): implement logic to set a default rule group during creation and update 2025-07-24 03:06:44 +09:00
Chang lue Tsen 994cc4bebb feat: add type and default fields to rule group requests and update related logic 2025-07-24 03:06:44 +09:00
Chang lue Tsen 40c24fbc85 fix(purchase): update gift amount deduction logic and handle zero-amount order status 2025-07-24 03:06:44 +09:00
Chang lue Tsen 59348c1643 fix(database): correct name entry for SingBox in initialization script 2025-07-24 03:06:44 +09:00
Chang lue Tsen 9691257bad fix(purchase): correct gift amount deduction logic and enhance payment processing comments 2025-07-07 14:26:53 -04:00
Chang lue Tsen 76816ca8ea fix(purchase): handle gift amount deduction and update order status in checkout logic 2025-07-07 12:28:44 -04:00
Chang lue Tsen e279119524 fix(user): update user balance, gift amount, and commission fields in user info 2025-07-07 12:11:59 -04:00
sky-line-1 626c3ef05c fix(api) fill renewal_order_users in user statistics (#40) 2025-07-06 15:39:42 +08:00
Chang lue Tsen 4f49dea769 feat(anytls): add AnyTLS protocol support with parsing and configuration options 2025-07-05 14:30:12 -04:00
Chang lue Tsen 1a849fd461 fix(api): standardize formatting and add missing newlines in API files 2025-07-05 14:30:05 -04:00
Chang lue Tsen 987e25e7ac fix(tuic): add additional configuration options for SNI, RTT reduction, UDP relay, and congestion control 2025-07-03 14:45:42 -04:00
ctrlbbj 189f62781e Fix schema missing
Adding the missing fields in the database initialization file.
2025-07-03 02:22:27 +09:00
Chang lue Tsen 9fc367a323 fix(statistics): add mock data for revenue and user statistics in demo mode 2025-07-02 13:18:34 -04:00
Chang lue Tsen 0825ac525d fix(user): prevent deletion and password modification of admin user in demo mode 2025-07-02 12:39:30 -04:00
sky-line-1 d26bc7f40b fix(order): correct renewal traffic reset logic (#37) 2025-07-01 23:07:27 +08:00
Chang lue Tsen be7cdcd4a6 fix(node): enqueue task for GetNodeCountry only if city or country is empty 2025-06-12 23:57:06 -04:00
Chang lue Tsen b6a2f134bb fix(parse): enable UDP support in VLESS configuration 2025-06-06 11:23:20 -04:00
Chang lue Tsen 76063bc071 fix(server): encode server key in base64 for shadowsocks protocol 2025-06-01 12:54:48 -04:00
missish 565eb8cbbf "fix(order): 修复优惠券使用限制检查逻辑,增加数量有效性验证" 2025-06-02 01:47:16 +09:00
Chang lue Tsen 617768a4a1 fix(proxy): update server key encoding in GenerateShadowsocks2022Password 2025-06-01 12:01:02 -04:00
Leif Draven aae4a10947 Merge pull request #23 from missish/develop
"feat(user/subscribe): 添加订阅折扣信息的JSON解析逻辑"
2025-06-01 22:11:04 +09:00
missish 6027102d29 Merge branch 'perfect-panel:develop' into develop 2025-06-01 14:30:54 +08:00
missish 5ad3202626 "feat(user/subscribe): 添加订阅折扣信息的JSON解析逻辑"
- 修复在续订时只能续订一个月
2025-06-01 14:20:57 +08:00
Chang lue Tsen 0dca618f85 fix(uri): update query parameters and improve transport type handling 2025-06-01 01:33:30 -04:00
Chang lue Tsen 852bde35dc refactor(v2rayN): streamline proxy URI generation by consolidating methods 2025-05-31 11:44:18 -04:00
Chang lue Tsen b4a9bb23e9 fix(proxy): simplify password generation logic in GenerateShadowsocks2022Password 2025-05-31 11:30:04 -04:00
Chang lue Tsen 0fb0aac2fa feat(shadowsocks): implement support for SIP022 AEAD-2022 ciphers 2025-05-31 11:28:06 -04:00
Chang lue Tsen 1a21984c27 feat(shadowsocks): SIP022 AEAD-2022 Ciphers 2025-05-31 10:46:28 -04:00
Chang lue Tsen ddf740600f fix(subscribe): improve logging and update query logic for server filtering 2025-05-31 09:30:13 -04:00
Chang lue Tsen 77f55f8708 style(api): standardize formatting and remove trailing newlines in API definitions 2025-05-31 09:29:31 -04:00
Leif Draven 56f6bd2542 Merge pull request #22 from missish/develop
refactor(tool): 增强复制功能并修复零值处理问题
2025-05-31 21:47:07 +09:00
missish 3ee43b14c6 Revert "chore: add ppanel.yaml to .gitignore for security"
This reverts commit a4eca9f9a6.
2025-05-31 11:53:40 +08:00
missish a4eca9f9a6 chore: add ppanel.yaml to .gitignore for security 2025-05-31 11:49:47 +08:00
missish e0a4bb028b fix(admin/server): 修复节点更新时Tags字段处理逻辑
修改DeepCopy调用以忽略空值,并完善Tags字段的处理逻辑。当Tags为空数组时清空数据库字段,避免保留旧值。
2025-05-31 11:22:02 +08:00
missish 581e3ee48a refactor(tool): 为DeepCopy和ShallowCopy添加配置选项支持
为copy工具函数添加了CopyOption配置选项,允许调用方自定义复制行为。主要修改包括:
1. 新增CopyOption函数类型和CopyWithIgnoreEmpty选项
2. 修改DeepCopy和ShallowCopy函数签名以支持可变选项参数
3. 重构内部实现以应用传入的选项配置

这使得调用方可以灵活控制是否忽略空值等复制行为,增强了工具函数的可配置性。
2025-05-31 10:40:36 +08:00
Leif Draven dbb585ddb5 Merge pull request #21 from missish/develop
fix(order): Corrected Count and UserLimit to 0, which should be unlimited
2025-05-30 21:27:58 +09:00
missish 207ffc854e fix(order): 增加优惠券使用限制的条件判断 2025-05-27 21:15:17 +08:00
lyndon986 d4089a801d Merge pull request #19 from darwinchow/fix/stripe-webhook-endpoint 2025-05-24 02:51:11 +07:00
Darwin 5cca0e7aa3 fix(payment): update Stripe webhook endpoint url 2025-05-22 14:28:32 +08:00
Chang lue Tsen 42a99c4925 feat(cache): add cache options with customizable expiry settings 2025-05-11 04:39:22 -04:00
Chang lue Tsen 9e20b270e7 fix(develop): update docker command to check for all containers instead of running ones 2025-05-06 10:50:21 -04:00
Chang lue Tsen 155c6ba290 feat(adapter): add V2rayN adapter and build method for proxy management 2025-05-06 18:36:35 +09:00
Chang lue Tsen f4050ff42e fix(model): update query logic to use OR for group_id filtering 2025-05-06 18:36:09 +09:00
Chang lue Tsen ae784cbbe4 fix(server): enhance sorting logic to handle multiple entries and reorder correctly 2025-05-06 14:27:11 +09:00
Chang lue Tsen a198c770db fix(server): update sorting logic to use FindOne and Update methods for better error handling 2025-05-06 14:07:46 +09:00
Chang lue Tsen a2e4c6d09a fix(subscribe): add BeforeDelete and BeforeUpdate methods for sort handling 2025-05-06 13:29:39 +09:00
Chang lue Tsen d6f5160be1 fix(server): remove reorderSort function to streamline server sorting logic 2025-05-06 13:26:59 +09:00
Chang lue Tsen d5f14bf665 fix(server): simplify BeforeDelete method by removing unnecessary Model call 2025-05-06 13:25:38 +09:00
Chang lue Tsen e625d9aed5 fix(server): update BeforeDelete method to use Model for sort update query 2025-05-06 13:24:20 +09:00
Chang lue Tsen 3cadd9e621 fix(server): refactor server model methods to support transaction handling 2025-05-06 13:21:27 +09:00
Chang lue Tsen 38bcacc9e7 fix(subscribe): update subscription availability check to allow status greater than 1 2025-05-06 12:05:33 +09:00
Chang lue Tsen ea74e4b7d5 fix(subscribe): update subscription query to include zero expiration time condition 2025-05-06 11:49:21 +09:00
Chang lue Tsen ead1a9a8a6 fix(subscribe): improve order details retrieval logic in resetUserSubscribeTokenLogic 2025-05-05 10:55:46 -04:00
Chang lue Tsen aef567c06f fix(subscribe): update subscribe record update logic to use ID instead of token 2025-05-05 10:43:12 -04:00
Chang lue Tsen 6912e281f3 fix(subscribe): correct subscription status update logic on renewal 2025-05-05 04:30:49 -04:00
Chang lue Tsen c5b0507187 fix(subscribe): enhance server tag handling in subscription logic 2025-05-05 04:04:18 -04:00
Chang lue Tsen 9a1942f8cf fix(server): update server filtering to support multiple tags 2025-04-30 12:53:21 +09:00
Chang lue Tsen f856dc7c01 fix(server): update server filtering to support multiple tags 2025-04-30 12:53:01 +09:00
Chang lue Tsen cdc3d8b8f1 fix(subscribe): escape site name in content-disposition headers 2025-04-29 19:21:58 +09:00
Chang lue Tsen 55ef348135 fix(proxy): replace string literals with constants for proxy group types 2025-04-29 18:09:53 +09:00
Chang lue Tsen 6d13060aeb fix(proxy): improve proxy removal logic in build functions 2025-04-29 17:02:17 +09:00
Chang lue Tsen 94416d59a6 fix(rules): remove unnecessary comments from final rules in configuration files 2025-04-29 16:19:03 +09:00
Chang lue Tsen bbe7b8c223 fix(docs): update download URLs in installation guides for server binaries 2025-04-29 15:19:10 +09:00
Chang lue Tsen fdf51a60e9 fix(docs): update architecture diagram links in English and Chinese README files 2025-04-29 14:47:39 +09:00
Leif Draven fd1e2857c8 Update architecture.svg
docs: architecture svg
2025-04-29 12:57:30 +09:00
Chang lue Tsen de74bd1d70 fix(subscribe): improve query logic and handle status filtering 2025-04-29 12:46:19 +09:00
Chang lue Tsen fd1104784b fix(docs): update Go Report Card badge link in README files 2025-04-28 11:43:59 -04:00
Chang lue Tsen f4f0756ba6 fix(docs): update CI/CD badge links in README files 2025-04-28 11:41:09 -04:00
Chang lue Tsen 08bc2f6331 fix(docs): update license badge links in README files 2025-04-28 11:32:46 -04:00
Chang lue Tsen dafe211ec7 fix(subscribe): add FinishedAt field to track subscription completion time 2025-04-28 11:18:53 -04:00
Chang lue Tsen 1fee981a58 fix(subscribe): update status and finished_at fields in subscription logic 2025-04-28 11:13:16 -04:00
Chang lue Tsen 2aaf4cf618 fix(subscribe): improve query logic and handle status filtering 2025-04-28 18:59:06 +09:00
Chang lue Tsen 105ee9cc46 fix(proxy): filter out empty proxy groups 2025-04-28 17:31:07 +09:00
Chang lue Tsen 3fd71a472b docs: update bug report and feature request links in English and Chinese README files 2025-04-27 11:25:07 +09:00
Chang lue Tsen 24e2aaeb61 docs: update header level for Special Thanks section in README 2025-04-27 11:23:09 +09:00
Chang lue Tsen a1016eef25 docs: update section header for special thanks in Chinese README 2025-04-27 11:21:29 +09:00
Chang lue Tsen af6fe2eeed docs: update README files with project links and add special thanks section 2025-04-27 11:19:53 +09:00
Chang lue Tsen b2dd1a3758 fix: add ppanel.yaml to .gitignore 2025-04-25 18:35:56 +09:00
Chang lue Tsen 9a747596e6 fix: add versioning support to Docker build process 2025-04-25 18:30:45 +09:00
Chang lue Tsen 4124dd43ce fix: update release workflow 2025-04-25 18:17:26 +09:00
Chang lue Tsen bc3f40fd69 fix: update release workflow 2025-04-25 18:13:45 +09:00
Chang lue Tsen 027596e3a8 fix: update release workflow 2025-04-25 18:03:49 +09:00
Chang lue Tsen 5b972514aa fix: update release workflow 2025-04-25 17:45:58 +09:00
Chang lue Tsen 5cccb1f8e3 refactor: simplify release workflow 2025-04-25 16:40:57 +09:00
Chang lue Tsen 579e32abd9 fix: update CI/CD badge link in README files 2025-04-25 15:38:52 +09:00
Chang lue Tsen f27b94dddf chore: rename readme.md to README.md and update Go version in documentation 2025-04-25 15:17:00 +09:00
Chang lue Tsen 81bf74e808 style: format code by adding newlines for improved readability 2025-04-25 15:10:27 +09:00
Chang lue Tsen 34c09c4618 refactor: update import paths from ppanel-server to server 2025-04-25 15:09:31 +09:00
Chang lue Tsen 52063049c5 fix: update version command to use constants for version and build time 2025-04-25 15:07:22 +09:00
Chang lue Tsen da4fd72105 chore: add Makefile for multi-platform build configuration 2025-04-25 15:03:54 +09:00
Chang lue Tsen 8addcc584b init: 1.0.0 2025-04-25 12:08:29 +09:00
314 changed files with 6279 additions and 10883 deletions
-244
View File
@@ -1,244 +0,0 @@
name: Build docker and publish
run-name: 简化的Docker构建和部署流程
on:
push:
branches:
- main
- dev
pull_request:
branches:
- main
- dev
env:
# Docker镜像仓库
REPO: ${{ vars.REPO || 'registry.kxsw.us/ario-server' }}
# SSH连接信息
SSH_HOST: ${{ vars.SSH_HOST }}
SSH_PORT: ${{ vars.SSH_PORT }}
SSH_USER: ${{ vars.SSH_USER }}
SSH_PASSWORD: ${{ vars.SSH_PASSWORD }}
# TG通知
TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
TG_CHAT_ID: "-4940243803"
# Go构建变量
SERVICE: ario
SERVICE_STYLE: ario
VERSION: ${{ github.sha }}
BUILDTIME: ${{ github.event.head_commit.timestamp }}
GOARCH: amd64
jobs:
build:
runs-on: ario-server
container:
image: node:20
strategy:
matrix:
# 只有node支持版本号别名
node: ['20.15.1']
steps:
# 步骤1: 下载代码
- name: 📥 下载代码
uses: actions/checkout@v4
# 步骤2: 设置动态环境变量
- name: ⚙️ 设置动态环境变量
run: |
if [ "${{ github.ref_name }}" = "main" ]; then
echo "DOCKER_TAG_SUFFIX=latest" >> $GITHUB_ENV
echo "CONTAINER_NAME=ppanel-server" >> $GITHUB_ENV
echo "DEPLOY_PATH=/root/vpn_server" >> $GITHUB_ENV
echo "为 main 分支设置生产环境变量"
elif [ "${{ github.ref_name }}" = "dev" ]; then
echo "DOCKER_TAG_SUFFIX=dev" >> $GITHUB_ENV
echo "CONTAINER_NAME=ppanel-server-dev" >> $GITHUB_ENV
echo "DEPLOY_PATH=/root/vpn_server_dev" >> $GITHUB_ENV
echo "为 dev 分支设置开发环境变量"
else
echo "DOCKER_TAG_SUFFIX=${{ github.ref_name }}" >> $GITHUB_ENV
echo "CONTAINER_NAME=ppanel-server-${{ github.ref_name }}" >> $GITHUB_ENV
echo "DEPLOY_PATH=/root/vpn_server_other" >> $GITHUB_ENV
echo "为其他分支 (${{ github.ref_name }}) 设置环境变量"
fi
# 步骤3: 安装系统工具 (curl, jq) 并升级 Docker CLI 到 1.44+
- name: 🔧 安装系统工具并升级 Docker CLI
run: |
set -e
export DEBIAN_FRONTEND=noninteractive
echo "等待 apt/dpkg 锁释放 (unattended-upgrades)..."
end=$((SECONDS+300))
while true; do
LOCKS_BUSY=0
if pgrep -x unattended-upgrades >/dev/null 2>&1; then LOCKS_BUSY=1; fi
if command -v fuser >/dev/null 2>&1; then
if fuser /var/lib/dpkg/lock >/dev/null 2>&1 \
|| fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
|| fuser /var/lib/apt/lists/lock >/dev/null 2>&1; then
LOCKS_BUSY=1
fi
fi
if [ "$LOCKS_BUSY" -eq 0 ]; then break; fi
if [ $SECONDS -ge $end ]; then
echo "等待 apt/dpkg 锁超时,使用 Dpkg::Lock::Timeout 继续..."
break
fi
echo "仍在等待锁释放..."; sleep 5
done
# 基础工具
apt-get update -y -o Dpkg::Lock::Timeout=600
apt-get install -y -o Dpkg::Lock::Timeout=600 jq curl ca-certificates gnupg lsb-release
# 移除旧版 docker.io,避免客户端过旧 (API 1.41)
if dpkg -s docker.io >/dev/null 2>&1; then
apt-get remove -y docker.io || true
fi
# 安装 Docker 官方仓库的 CLI (确保 API >= 1.44)
distro_codename=$(. /etc/os-release && echo "$VERSION_CODENAME")
install_repo="deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian ${distro_codename} stable"
mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "$install_repo" > /etc/apt/sources.list.d/docker.list
apt-get update -y -o Dpkg::Lock::Timeout=600
apt-get install -y -o Dpkg::Lock::Timeout=600 docker-ce-cli docker-buildx-plugin
# 版本检查
docker --version || true
docker version || true
echo "客户端 API 版本:" $(docker version --format '{{.Client.APIVersion}}')
# 步骤4: 构建并发布到镜像仓库
- name: 📤 构建并发布到镜像仓库
run: |
echo "开始构建并推送镜像..."
echo "仓库: ${{ env.REPO }}"
echo "版本标签: ${{ env.VERSION }}"
echo "分支标签: ${{ env.DOCKER_TAG_SUFFIX }}"
# 构建镜像,同时打上版本和分支两个标签
docker build -f Dockerfile \
--platform linux/amd64 \
--build-arg TARGETARCH=amd64 \
--build-arg VERSION=${{ env.VERSION }} \
--build-arg BUILDTIME=${{ env.BUILDTIME }} \
-t ${{ env.REPO }}:${{ env.VERSION }} \
-t ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }} \
.
echo "推送版本标签镜像: ${{ env.REPO }}:${{ env.VERSION }}"
docker push ${{ env.REPO }}:${{ env.VERSION }}
echo "推送分支标签镜像: ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}"
docker push ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}
echo "镜像推送完成"
# 步骤5: 连接服务器拉镜像启动
- name: 🚀 连接服务器拉镜像启动
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ env.SSH_HOST }}
username: ${{ env.SSH_USER }}
password: ${{ env.SSH_PASSWORD }}
port: ${{ env.SSH_PORT }}
timeout: 300s
command_timeout: 600s
script: |
echo "连接服务器成功,开始部署..."
echo "部署容器名: ${{ env.CONTAINER_NAME }}"
echo "部署路径: ${{ env.DEPLOY_PATH }}"
echo "部署镜像: ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}"
# 确保部署目录存在
mkdir -p ${{ env.DEPLOY_PATH }}/config
mkdir -p ${{ env.DEPLOY_PATH }}/logs
# 停止并删除旧容器(忽略所有错误)
if docker ps -a | grep -q ${{ env.CONTAINER_NAME }} 2>/dev/null; then
echo "停止旧容器..."
docker stop ${{ env.CONTAINER_NAME }} >/dev/null 2>&1 || true
echo "等待容器完全停止..."
sleep 5
echo "删除旧容器..."
# 静默删除,完全忽略错误输出
docker rm ${{ env.CONTAINER_NAME }} >/dev/null 2>&1 || true
sleep 2
# 如果仍然存在,尝试强制删除(静默)
if docker ps -a | grep -q ${{ env.CONTAINER_NAME }} 2>/dev/null; then
echo "尝试强制删除..."
docker rm -f ${{ env.CONTAINER_NAME }} >/dev/null 2>&1 || true
sleep 3
fi
echo "容器清理完成,继续部署..."
fi
# 拉取最新分支镜像
echo "拉取镜像: ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}..."
docker pull ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}
# 启动新容器
echo "启动新容器..."
cd ${{ env.DEPLOY_PATH }}
docker run -d \
--name ${{ env.CONTAINER_NAME }} \
--restart unless-stopped \
--network host \
-v ./config/ppanel.yaml:/app/etc/ppanel.yaml \
-v ./logs:/app/logs \
${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}
# 检查容器状态
sleep 5
if docker ps | grep -q ${{ env.CONTAINER_NAME }}; then
echo "✅ 容器启动成功"
else
echo "❌ 容器启动失败"
docker logs ${{ env.CONTAINER_NAME }}
exit 1
fi
# 步骤6: TG通知 (成功)
- name: 📱 发送成功通知到Telegram
if: success()
uses: appleboy/telegram-action@master
with:
token: ${{ env.TG_BOT_TOKEN }}
to: ${{ env.TG_CHAT_ID }}
message: |
✅ 部署成功!
📦 项目: ${{ github.repository }}
🌿 分支: ${{ github.ref_name }}
📝 提交: ${{ github.sha }}
👤 提交者: ${{ github.actor }}
🕐 时间: ${{ github.event.head_commit.timestamp }}
🚀 服务已成功部署到生产环境
parse_mode: Markdown
# 步骤5: TG通知 (失败)
- name: 📱 发送失败通知到Telegram
if: failure()
uses: appleboy/telegram-action@master
with:
token: ${{ env.TG_BOT_TOKEN }}
to: ${{ env.TG_CHAT_ID }}
message: |
❌ 部署失败!
📦 项目: ${{ github.repository }}
🌿 分支: ${{ github.ref_name }}
📝 提交: ${{ github.sha }}
👤 提交者: ${{ github.actor }}
🕐 时间: ${{ github.event.head_commit.timestamp }}
⚠️ 请检查构建日志获取详细信息
parse_mode: Markdown
+27
View File
@@ -0,0 +1,27 @@
# 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
+23
View File
@@ -0,0 +1,23 @@
# 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
+79
View File
@@ -0,0 +1,79 @@
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
+2 -1
View File
@@ -6,11 +6,12 @@
*.log
.DS_Store
*_test_config.go
*.log*
/build/
etc/ppanel.yaml
*.p8
*.crt
*.key
node_modules
package-lock.json
package.json
/bin
+130
View File
@@ -0,0 +1,130 @@
version: 2
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- "386"
- amd64
- arm64
ignore:
- goos: darwin
goarch: "386"
binary: ppanel-server
ldflags:
- -s -w
- -X "github.com/perfect-panel/server/pkg/constant.Version={{.Version}}"
- -X "github.com/perfect-panel/server/pkg/constant.BuildTime={{.Date}}"
- -X "github.com/perfect-panel/server/pkg/constant.GitCommit={{.Commit}}"
main: ./ppanel.go
archives:
- format: tar.gz
name_template: >-
{{ .ProjectName }}-
{{- .Version }}-
{{- title .Os }}-
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
files:
- LICENSE
- etc/*
format_overrides:
- goos: windows
format: zip
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
use: github
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
- Merge pull request
groups:
- title: Features
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: 'Bug fixes'
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: Others
order: 999
dockers:
- image_templates:
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:{{ .Tag }}"
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:v{{ .Major }}"
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:v{{ .Major }}.{{ .Minor }}"
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:latest"
dockerfile: Dockerfile
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--platform=linux/amd64"
use: docker
extra_files:
- etc/
- image_templates:
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:{{ .Tag }}-arm64"
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:v{{ .Major }}.{{ .Minor }}-arm64"
dockerfile: Dockerfile
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--platform=linux/arm64"
use: docker
goarch: arm64
extra_files:
- etc/
docker_signs:
- cmd: cosign
args:
- "sign"
- "${artifact}@${digest}"
env:
- COSIGN_EXPERIMENTAL=1
release:
github:
owner: perfect-panel
name: server
draft: false
prerelease: auto
name_template: "{{.ProjectName}} v{{.Version}}"
header: |
## ppanel-server {{.Version}}
Welcome to this new release!
footer: |
Docker images are available at:
- `{{ .Env.DOCKER_USERNAME }}/ppanel-server:{{ .Tag }}`
- `{{ .Env.DOCKER_USERNAME }}/ppanel-server:latest`
For more information, visit our documentation.
@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="go build github.com/perfect-panel/server" type="GoApplicationRunConfiguration" factoryName="Go Application" nameIsGenerated="true">
<module name="server" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="run --config etc/ppanel-dev.yaml" />
<kind value="PACKAGE" />
<package value="github.com/perfect-panel/server" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$/ppanel.go" />
<method v="2" />
</configuration>
</component>
@@ -1,112 +0,0 @@
## 目标
* 不使用自动续期订阅;采用“非续期订阅”或“非消耗型”作为内购模式。
* 仅实现 Go 后端 API;客户端(iOS/StoreKit 2)按说明调用。
## 产品模型
* 非续期订阅:固定时长通行证(如 30/90/365 天),产品ID`com.airport.vpn.pass.30d|90d|365d`
* 非消耗型(可选):一次性解锁某附加功能,产品ID:`com.airport.vpn.addon.xyz`
* 服务器以 `productId→权益/时长` 进行配置映射。
## 后端API设计(Go/Gin
* 路由注册:`internal/handler/routes.go`
* `GET /api/iap/apple/products`:返回前端展示的产品清单(含总价/描述/时长映射)
* `POST /api/iap/apple/transactions/attach`:绑定一次购买到用户账户(需登录)。入参:`signedTransactionJWS`
* `POST /api/iap/apple/restore`:恢复购买(批量接收 JWS 列表并绑定)
* `GET /api/iap/apple/status`:返回用户当前权益与到期时间(统一来源聚合)
* 逻辑目录:`internal/logic/iap/apple/*`
* `AttachTransactionLogic`:解析 JWS→校验 `bundleId/productId/purchaseDate`→根据 `productId` 映射权益与时长→更新订阅统一表
* `RestoreLogic`:对所有已购记录执行绑定去重(基于 `original_transaction_id`
* `QueryStatusLogic`:聚合各来源订阅,返回有效权益(取最近到期/最高等级)
* 工具包:`pkg/iap/apple`
* `ParseTransactionJWS`:解析 JWS,提取 `transactionId/originalTransactionId/productId/purchaseDate/revocationDate`
* `VerifyBasic`:基础校验(`bundleId`、签名头部与证书链存在性);如客户端已 `transaction.verify()`,可采用“信任+服务器最小校验”的模式快速落地
* 配置:`doc/config-zh.md`
* `IAP_PRODUCT_MAP``productId → tier/duration`(例如:`30d→+30天``addon→解锁功能X`
* `APPLE_IAP_BUNDLE_ID`:用于 JWS 内部校验
## 数据模型
* 新表:`apple_iap_transactions`
* `id``user_id``original_transaction_id`(唯一)、`transaction_id``product_id``purchase_at``revocation_at``jws_hash`
* 统一订阅表增强(现有 `SubscribeModel`
* 新增来源:`source=apple_iap``external_id=original_transaction_id``tier``expires_at`
* 索引:`original_transaction_id` 唯一、`user_id+source``expires_at`
## 与现有系统融合
* `internal/svc/serviceContext.go`:初始化 IAP 模块与模型
* `QueryPurchaseOrderLogic/SubscribeModel`:聚合苹果IAP来源;冲突策略:按最高权益与最晚到期。
* 不产生命令行支付订单,仅记录订阅流水与审计(避免与 Stripe 等混淆)。
## 安全与合规
* 仅显示商店在可支付时;价格、描述清晰;使用系统确认表单。
* 服务器进行最小校验:`bundleId``productId`白名单、`purchaseDate`有效性;保存 `jws_hash` 做去重。
* 退款:在 App 内提供“请求退款”的帮助页并使用系统接口触发;后端无需额外API。
## 客户端使用说明(StoreKit 2
* 产品拉取与展示:
* 通过已知 `productId` 列表调用 `Product.products(for:)`;展示总价与描述,检查 `canMakePayments`
* 购买:
* 调用 `purchase()`,系统确认表单弹出→返回 `Transaction`;执行 `await transaction.verify()`
* 成功后将 `transaction.signedData` POST 到 `/api/iap/apple/transactions/attach`
* 恢复:
* 调用 `Transaction.currentEntitlements`,遍历并验证每条 `Transaction`,将其 `signedData` 批量 POST 到 `/api/iap/apple/restore`
* 状态显示:
* 访问 `GET /api/iap/apple/status` 获取到期时间与权益用于 UI 展示
* 退款入口:
* 在购买帮助页直接使用 `beginRefundRequest(for:in:)`;文案简洁,按钮直达
## 测试与验收
* 单元测试:JWS 解析、`productId→权益/时长` 映射、去重策略。
* 集成测试:绑定/恢复接口鉴权与幂等、统一订阅查询结果。
* 沙盒:使用 iOS 沙盒购买与恢复;记录审计与日志。
## 里程碑
1. 基础能力:`products/status``transactions/attach` 落地
2. 恢复与融合:`restore` + 统一订阅聚合
3. 上线前验证:沙盒测试与文案、监控
@@ -1,44 +0,0 @@
# 用户管理系统优化方案 (最终确认版)
根据您的要求,我们将重点实现 `last_login_time` 字段的存储与返回,以及在列表接口中聚合会员套餐信息。
## 实施步骤
### 1. 数据库变更
- **文件**: `initialize/migrate/database/02121_add_user_last_login_time.up.sql`
- **内容**:
```sql
ALTER TABLE user ADD COLUMN last_login_time DATETIME DEFAULT NULL COMMENT 'Last Login Time';
```
- **说明**: 相比查询日志表,直接在用户表增加字段能极大提高列表页查询性能。
### 2. API 定义更新
- **文件**: `apis/types.api`
- **内容**: 修改 `User` 结构体,增加以下返回字段:
- `last_login_time` (int64): 最后活跃时间戳。
- `member_status` (string): 会员状态(显示当前生效的订阅套餐名称,无订阅显示空或特定标识)。
### 3. 后端模型与逻辑更新
#### 3.1 User 模型更新
- **文件**: `internal/model/user/user.go`
- **内容**: `User` 结构体增加 `LastLoginTime *time.Time` 字段。
#### 3.2 登录逻辑更新 (记录活跃时间)
- **文件**: `internal/logic/auth/userLoginLogic.go` (及其他登录逻辑如 `emailLoginLogic.go`)
- **内容**: 在登录成功后,异步或同步更新当前用户的 `last_login_time`。
#### 3.3 用户列表逻辑更新 (数据聚合)
- **文件**: `internal/logic/admin/user/getUserListLogic.go`
- **内容**:
1. **获取用户列表**: 包含新增的 `LastLoginTime` 数据。
2. **批量查询订阅**: 根据当前页的用户 ID 列表,批量查询其**活跃订阅** (Active Subscription)。
3. **数据组装**:
- 将 `LastLoginTime` 转换为时间戳返回。
- 将订阅的 `Name` (套餐名) 赋值给 `member_status`。
### 4. 文档更新
- **文件**: `doc/说明文档.md`
- **内容**: 更新进度记录,标记完成“最后活跃”与“会员状态”字段开发。
## 验证与交付
- 提供 `curl` 验证命令,确认 `/v1/admin/user/list` 接口返回的 JSON 中包含 `last_login_time` 和 `member_status`。
@@ -1,67 +0,0 @@
## 修复目标
- 解决首次设备登录时在 `internal/logic/auth/deviceLoginLogic.go:99``deviceInfo` 赋值导致的空指针崩溃,确保接口稳定返回。
## 根因定位
- 设备不存在分支仅创建用户与设备记录,但未为局部变量 `deviceInfo` 赋值;随后在 `internal/logic/auth/deviceLoginLogic.go:99-100` 使用 `deviceInfo` 导致 `nil` 解引用。
- 参考位置:
- 赋值处:`internal/logic/auth/deviceLoginLogic.go:99-101`
- 设备存在分支赋值:`internal/logic/auth/deviceLoginLogic.go:88-95`
- 设备不存在分支未赋值:`internal/logic/auth/deviceLoginLogic.go:74-79`
- `UpdateDevice` 需要有效设备 `Id``internal/model/user/device.go:58-69`
## 修改方案
1. 在“设备不存在”分支注册完成后,立即通过标识重新查询设备,赋值给 `deviceInfo`
-`internal/logic/auth/deviceLoginLogic.go``if errors.Is(err, gorm.ErrRecordNotFound)` 分支中,`userInfo, err = l.registerUserAndDevice(req)` 之后追加:
- `deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)`
- 如果查询失败则返回数据库查询错误(与现有风格一致)。
2. 在更新设备 UA 前增加空指针保护,并不再忽略更新错误:
-`internal/logic/auth/deviceLoginLogic.go:99-101` 改为:
- 检查 `deviceInfo != nil`
- `deviceInfo.UserAgent = req.UserAgent`
- `if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil {` 记录错误并返回包装后的错误 `xerr.DatabaseUpdateError`
3. 可选优化(减少二次查询):
-`registerUserAndDevice(req)` 的返回值改为 `(*user.User, *user.Device, error)`,在注册时直接返回新建设备对象;调用点随之调整。若选择此方案,仍需在更新前做空指针保护。
## 代码示例(方案1,最小改动)
```go
// internal/logic/auth/deviceLoginLogic.go
// 设备不存在分支注册后追加一次设备查询
userInfo, err = l.registerUserAndDevice(req)
if err != nil {
return nil, err
}
deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
if err != nil {
l.Errorw("query device after register failed",
logger.Field("identifier", req.Identifier),
logger.Field("error", err.Error()),
)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device after register failed: %v", err.Error())
}
// 更新 UA,不忽略更新错误
if deviceInfo != nil {
deviceInfo.UserAgent = req.UserAgent
if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil {
l.Errorw("update device failed",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier),
logger.Field("error", err.Error()),
)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error())
}
}
```
## 测试用例与验证
- 用例1:首次设备标识登录(设备不存在)应成功返回 Token,日志包含注册与登录记录,无 500。
- 用例2:已存在设备标识登录(设备存在)应正常更新 UA 并返回 Token。
- 用例3:模拟数据库异常时应返回一致的业务错误码,不产生 `panic`
## 风险与回滚
- 改动限定在登录逻辑,属最小范围;若出现异常,回滚为当前版本即可。
- 不改变数据结构与外部接口行为,兼容现有客户端。
## 后续优化(可选)
- 统一 `UpdateDevice` 错误处理路径,避免 `_ = ...` 静默失败。
- 为“首次设备登录”场景补充集成测试,保证不再回归。
@@ -1,65 +0,0 @@
## 目标
- 不使用自动续期订阅;采用“非续期订阅”或“非消耗型”作为内购模式。
- 仅实现 Go 后端 API;客户端(iOS/StoreKit 2)按说明调用。
## 产品模型
- 非续期订阅:固定时长通行证(如 30/90/365 天),产品ID`com.airport.vpn.pass.30d|90d|365d`
- 非消耗型(可选):一次性解锁某附加功能,产品ID:`com.airport.vpn.addon.xyz`
- 服务器以 `productId→权益/时长` 进行配置映射。
## 后端API设计(Go/Gin
- 路由注册:`internal/handler/routes.go`
- `GET /api/iap/apple/products`:返回前端展示的产品清单(含总价/描述/时长映射)
- `POST /api/iap/apple/transactions/attach`:绑定一次购买到用户账户(需登录)。入参:`signedTransactionJWS`
- `POST /api/iap/apple/restore`:恢复购买(批量接收 JWS 列表并绑定)
- `GET /api/iap/apple/status`:返回用户当前权益与到期时间(统一来源聚合)
- 逻辑目录:`internal/logic/iap/apple/*`
- `AttachTransactionLogic`:解析 JWS→校验 `bundleId/productId/purchaseDate`→根据 `productId` 映射权益与时长→更新订阅统一表
- `RestoreLogic`:对所有已购记录执行绑定去重(基于 `original_transaction_id`
- `QueryStatusLogic`:聚合各来源订阅,返回有效权益(取最近到期/最高等级)
- 工具包:`pkg/iap/apple`
- `ParseTransactionJWS`:解析 JWS,提取 `transactionId/originalTransactionId/productId/purchaseDate/revocationDate`
- `VerifyBasic`:基础校验(`bundleId`、签名头部与证书链存在性);如客户端已 `transaction.verify()`,可采用“信任+服务器最小校验”的模式快速落地
- 配置:`doc/config-zh.md`
- `IAP_PRODUCT_MAP``productId → tier/duration`(例如:`30d→+30天``addon→解锁功能X`
- `APPLE_IAP_BUNDLE_ID`:用于 JWS 内部校验
## 数据模型
- 新表:`apple_iap_transactions`
- `id``user_id``original_transaction_id`(唯一)、`transaction_id``product_id``purchase_at``revocation_at``jws_hash`
- 统一订阅表增强(现有 `SubscribeModel`
- 新增来源:`source=apple_iap``external_id=original_transaction_id``tier``expires_at`
- 索引:`original_transaction_id` 唯一、`user_id+source``expires_at`
## 与现有系统融合
- `internal/svc/serviceContext.go`:初始化 IAP 模块与模型
- `QueryPurchaseOrderLogic/SubscribeModel`:聚合苹果IAP来源;冲突策略:按最高权益与最晚到期。
- 不产生命令行支付订单,仅记录订阅流水与审计(避免与 Stripe 等混淆)。
## 安全与合规
- 仅显示商店在可支付时;价格、描述清晰;使用系统确认表单。
- 服务器进行最小校验:`bundleId``productId`白名单、`purchaseDate`有效性;保存 `jws_hash` 做去重。
- 退款:在 App 内提供“请求退款”的帮助页并使用系统接口触发;后端无需额外API。
## 客户端使用说明(StoreKit 2
- 产品拉取与展示:
- 通过已知 `productId` 列表调用 `Product.products(for:)`;展示总价与描述,检查 `canMakePayments`
- 购买:
- 调用 `purchase()`,系统确认表单弹出→返回 `Transaction`;执行 `await transaction.verify()`
- 成功后将 `transaction.signedData` POST 到 `/api/iap/apple/transactions/attach`
- 恢复:
- 调用 `Transaction.currentEntitlements`,遍历并验证每条 `Transaction`,将其 `signedData` 批量 POST 到 `/api/iap/apple/restore`
- 状态显示:
- 访问 `GET /api/iap/apple/status` 获取到期时间与权益用于 UI 展示
- 退款入口:
- 在购买帮助页直接使用 `beginRefundRequest(for:in:)`;文案简洁,按钮直达
## 测试与验收
- 单元测试:JWS 解析、`productId→权益/时长` 映射、去重策略。
- 集成测试:绑定/恢复接口鉴权与幂等、统一订阅查询结果。
- 沙盒:使用 iOS 沙盒购买与恢复;记录审计与日志。
## 里程碑
1) 基础能力:`products/status``transactions/attach` 落地
2) 恢复与融合:`restore` + 统一订阅聚合
3) 上线前验证:沙盒测试与文案、监控
@@ -1,92 +0,0 @@
## 修复目标
* 解决首次设备登录时在 `internal/logic/auth/deviceLoginLogic.go:99``deviceInfo` 赋值导致的空指针崩溃,确保接口稳定返回。
## 根因定位
* 设备不存在分支仅创建用户与设备记录,但未为局部变量 `deviceInfo` 赋值;随后在 `internal/logic/auth/deviceLoginLogic.go:99-100` 使用 `deviceInfo` 导致 `nil` 解引用。
* 参考位置:
* 赋值处:`internal/logic/auth/deviceLoginLogic.go:99-101`
* 设备存在分支赋值:`internal/logic/auth/deviceLoginLogic.go:88-95`
* 设备不存在分支未赋值:`internal/logic/auth/deviceLoginLogic.go:74-79`
* `UpdateDevice` 需要有效设备 `Id``internal/model/user/device.go:58-69`
## 修改方案
1. 在“设备不存在”分支注册完成后,立即通过标识重新查询设备,赋值给 `deviceInfo`
*`internal/logic/auth/deviceLoginLogic.go``if errors.Is(err, gorm.ErrRecordNotFound)` 分支中,`userInfo, err = l.registerUserAndDevice(req)` 之后追加:
* `deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)`
* 如果查询失败则返回数据库查询错误(与现有风格一致)。
2. 在更新设备 UA 前增加空指针保护,并不再忽略更新错误:
*`internal/logic/auth/deviceLoginLogic.go:99-101` 改为:
* 检查 `deviceInfo != nil`
* `deviceInfo.UserAgent = req.UserAgent`
* `if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil {` 记录错误并返回包装后的错误 `xerr.DatabaseUpdateError`
3. 可选优化(减少二次查询):
*`registerUserAndDevice(req)` 的返回值改为 `(*user.User, *user.Device, error)`,在注册时直接返回新建设备对象;调用点随之调整。若选择此方案,仍需在更新前做空指针保护。
## 代码示例(方案1,最小改动)
```go
// internal/logic/auth/deviceLoginLogic.go
// 设备不存在分支注册后追加一次设备查询
userInfo, err = l.registerUserAndDevice(req)
if err != nil {
return nil, err
}
deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
if err != nil {
l.Errorw("query device after register failed",
logger.Field("identifier", req.Identifier),
logger.Field("error", err.Error()),
)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device after register failed: %v", err.Error())
}
// 更新 UA,不忽略更新错误
if deviceInfo != nil {
deviceInfo.UserAgent = req.UserAgent
if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil {
l.Errorw("update device failed",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier),
logger.Field("error", err.Error()),
)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error())
}
}
```
## 测试用例与验证
* 用例1:首次设备标识登录(设备不存在)应成功返回 Token,日志包含注册与登录记录,无 500。
* 用例2:已存在设备标识登录(设备存在)应正常更新 UA 并返回 Token。
* 用例3:模拟数据库异常时应返回一致的业务错误码,不产生 `panic`
## 风险与回滚
* 改动限定在登录逻辑,属最小范围;若出现异常,回滚为当前版本即可。
* 不改变数据结构与外部接口行为,兼容现有客户端。
## 后续优化(可选)
* 统一 `UpdateDevice` 错误处理路径,避免 `_ = ...` 静默失败。
* 为“首次设备登录”场景补充集成测试,保证不再回归。
@@ -1,33 +0,0 @@
## 结论与定位
- 解绑接口路径:`/v1/public/user/unbind_device`,路由注册于 `internal/handler/routes.go:836-838`,处理器在 `internal/handler/public/user/unbindDeviceHandler.go:11-25`
- 业务逻辑:`internal/logic/public/user/unbindDeviceLogic.go:36-141`,进行设备所属用户迁移、认证记录更新、可能删除旧用户以及 Redis 缓存清理。
- 返回封装:所有接口错误均走 JSON 200 包装,见 `pkg/result/httpResult.go:12-33`,因此应用层不会主动返回 502。
- Nginx 反向代理:API 代理到 `127.0.0.1:8080`,见 `etc/nginx.conf:233-260`;未设置显式的 `proxy_read_timeout` 等超时参数,502 多发生在上游超时或连接被复位。
## 高概率问题点
- 非安全类型断言导致潜在 panic`internal/logic/public/user/unbindDeviceLogic.go:38` 直接 `.(*user.User)` 断言,若上下文未注入用户(Token 缺失/失效、链路异常),将发生 panic。其他多数逻辑使用安全断言并兜底(如 `internal/logic/public/user/unbindOAuthLogic.go:31-36`)。
- 同类不安全用法还出现在 `internal/logic/public/user/getDeviceListLogic.go:31-33`(你反馈其他接口正常,但它也可能受影响,建议同修)。
- 事务中混入外部 IO:在 DB 事务闭包内进行 Redis 删除(`internal/logic/public/user/unbindDeviceLogic.go:125-131`),若外部 IO 波动叠加数据库锁等待,整体耗时可能逼近或超过代理默认超时,引发 502。
## 修复方案
- 将所有 `CtxKeyUser` 获取统一改为安全断言:
- 失败时返回 `InvalidAccess` 业务错误而非 panic,参考 `unbindOAuth` 的处理方式;修改位置:`internal/logic/public/user/unbindDeviceLogic.go:36-43``internal/logic/public/user/getDeviceListLogic.go:31-33`
- 将 Redis 缓存清理移出事务闭包,并添加超时控制:
- 事务仅做数据库一致性操作;在事务成功后再进行缓存删除,失败则不删;为 Redis 操作设置短超时,避免阻塞主流程。
- 增强可观测性:
- 在解绑入口和事务前后增加结构化日志,包含 `device_id``user_id`、事务耗时、Redis 耗时、错误栈;方便精确定位是否存在偶发长耗时。
- 代理层稳健性:
- 在 Nginx 针对 API 站点增加 `proxy_connect_timeout 10s; proxy_send_timeout 60s; proxy_read_timeout 60s;`,并开启 `proxy_next_upstream timeout`;避免上游短抖动即直接 502。
## 验证计划
- 单元测试:构造上下文缺失用户的请求,确认不再 panic,返回业务错误码而非 502。
- 事务耗时压测:模拟高并发解绑,观察事务与 Redis 操作耗时分布,确认在代理超时阈值内。
- 端到端验证:
- 使用有效与无效 Token、不同设备 ID 连续调用解绑,确认响应始终为 200 JSON 包装(或业务错误),不出现 502。
- 对比 Nginx 错误日志,验证 502 消失。
## 变更范围
- 代码改动:`internal/logic/public/user/unbindDeviceLogic.go``internal/logic/public/user/getDeviceListLogic.go` 增加安全断言与错误返回;调整 Redis 清理至事务外并加超时;补充必要日志。
- 配置更新:`etc/nginx.conf` 的 API server 段增加代理超时与上游重试策略。
请确认以上方案,我将按该方案实施、编写必要函数注释并补充测试,完成后提供验证结果与影响评估。
@@ -1,31 +0,0 @@
## 问题确认
- 前端以“百分比”形式配置折扣(如 95、95.19),当前后端 `getDiscount` 函数期望“系数(0–1)”,导致折扣未生效。
- 受影响位置:
- 登录下单折扣:`internal/logic/public/order/getDiscount.go`
- 门户预下单/下单折扣:`internal/logic/public/portal/tool.go:getDiscount`
## 改造目标
- 后端折扣计算统一兼容两种输入:
- 系数(01):直接使用
- 百分比(>1 且 <=100):自动转换为 `值/100` 再使用
- 对非法值进行边界保护(<0 → 0;>100 → 忽略或按 1 处理),避免异常。
## 实施步骤
1. 修改折扣计算函数:
- `internal/logic/public/order/getDiscount.go`
-`discount.Discount > 1 && discount.Discount <= 100`,转换为 `discount.Discount/100`
- 保持“取满足阈值的最小折扣”策略,默认 `finalDiscount=1.0`
- `internal/logic/public/portal/tool.go:getDiscount`
- 同上逻辑,移除对 `*100` 的中间整数化处理,统一用浮点小数系数比较。
2. 单元测试补充:
- 既测系数(如 0.95),也测百分比输入(如 95、95.19),以及边界(0、100、>100)。
3. 验证流程:
- 用你当前 7 天的配置(前端百分比)进行“预下单→下单→订单查询”,确认 `Discount``Amount` 按预期生效。
4. 文档与界面提示:
- 在后台/前端表单处增加说明:支持百分比与系数;百分比将自动转换;推荐使用百分比,避免歧义。
## 交付与保障
- 代码改动仅限折扣计算函数与测试,风险低;保留原有行为的向后兼容。
- 提供测试报告与一次联调记录(数据截图:价格、折扣、总计)。
请确认是否按该兼容方案执行,我将据此修改并验证。
@@ -1,47 +0,0 @@
## 实施目标
- 复用现有订单与队列赋权,接入 Apple 自动续期(IAP),保持报表/审计/通知一致。
## 方案选择
- 采用“平台化复用 + 合成订单”的方式:Apple 由客户端结算 + 服务器通知驱动,服务端生成“已支付订单”进入现有赋权与续费流程。
## 具体改动(按文件)
1) 平台标识
- 更新 `pkg/payment/platform.go`:新增 `AppleIAP` 枚举与名称(仅标识,不参与 `PurchaseCheckout`)。
2) 路由与 Handler
- 新增公共接口:`POST /v1/public/iap/verify`
- 位置:`internal/handler/public/iap/verifyHandler.go`
- 逻辑:调用 `internal/logic/public/iap/verifyLogic.go`,以 `originalTransactionId` 验证 Apple 购买,生成“已支付订阅订单”,入队激活。
- 新增通知接口:`POST /v1/iap/notifications`
- 位置:`internal/handler/notify/appleIAPNotifyHandler.go`
- 逻辑:调用 `internal/logic/notify/appleIAPNotifyLogic.go`,JWS 验签后按事件(初购/续期/退款)生成或更新订单,触发续费或撤销权益。
- 路由注册:
- `internal/handler/routes.go` 增加 `/v1/public/iap/verify` 路由。
- `internal/handler/notify.go` 增加独立 `/v1/iap/notifications` 路由(Apple 不带 `:token`)。
3) 数据与模型
- 在用户订阅(或新建 `iap_binding` 表)绑定:`originalTransactionId``environment``latestExpiresDate`
- 订单字段复用:`Method=AppleIAP``TradeNo=originalTransactionId``Type=1/2`(订阅/续费),`Status=2`(已支付),金额可取通知中的价格;取不到则置 `Amount=0` 保证流程。
4) 逻辑复用与改造点
- 赋权:复用 `queue/logic/order/activateOrderLogic.go:165 NewPurchase`
- 续费:复用 `queue/logic/order/activateOrderLogic.go:529 updateSubscriptionForRenewal`
- 不改动 `internal/logic/public/portal/purchaseCheckoutLogic.go` 的渠道路由(Apple 不走此流程)。
5) 安全与幂等
- Apple JWS 验签:拉取并缓存 JWKS 公钥,校验通知;拒绝无效签名。
- 幂等:以 `notificationId`/`transactionId``originalTransactionId` 去重处理。
6) 客户端协作
- iOS:完成 StoreKit 购买后携带 `originalTransactionId` 调用 `/v1/public/iap/verify`
- 续费:依赖 Server Notifications v2 自动驱动,无需客户端调用。
7) 测试与监控
- 沙盒验证初购、续期、重试与宽限期、退款撤销;注意元数据延迟(~1小时)。
- 指标:通知验签失败、API 调用失败、幂等冲突、状态不一致报警。
## 交付节奏
- 第一步:平台枚举与路由骨架;
- 第二步:`verify` 验证与“合成订单”生成;
- 第三步:通知验签与事件映射;
- 第四步:沙盒联调,确认队列赋权与续费延长。
@@ -1,92 +0,0 @@
## 结论
* 可以复用你现有的“订单→支付成功→订单激活(赋权)→通知/返佣”的主干流程,但“支付环节”不能复用第三方网关逻辑,必须改为 Apple IAP 的校验与事件驱动。
* 复用范围:订单模型、续费与赋权队列、优惠/手续费计算、通知与返佣;差异点:支付下单与回调换成“StoreKit 客户端购买 + 服务端向 Apple 校验 + Apple Server Notifications v2”。
## 可复用的部分
1. 订单激活与赋权
* 新购赋权:`queue/logic/order/activateOrderLogic.go:164-193``NewPurchase`
* 续费赋权:`queue/logic/order/activateOrderLogic.go:473-515``Renewal`
* 流量重置与充值:`queue/logic/order/activateOrderLogic.go:564-626`, `630-675`
1. 订单与费用模型
* 订单结构:`internal/model/order/order.go:5-29` 可继续承载 IAP 订单(新增字段映射 Apple 交易)
* 费用/折扣/礼金计算逻辑保持不变
1. 队列驱动
* 仍使用“支付成功→入队→处理”的模式:`queue/logic/order/activateOrderLogic.go:65-86`
## 必须独立实现的部分
1. Apple IAP 支付与校验
* 客户端使用 StoreKit 购买,拿到 `originalTransactionId`
* 服务端调用 App Store Server API,基于 `originalTransactionId` 校验订阅有效性并取交易历史
1. Apple Server Notifications v2
* 在 App Store Connect 配置通知 URL
* 服务端实现 JWS 验签,解析事件并落库:续期、失败、宽限期、退款、撤销等
## 整合方式(复用策略)
1. 引入平台枚举“AppleIAP”
*`pkg/payment/platform.go` 增加 `AppleIAP`,用于平台标识与管理端展示
1. 订单创建策略(两种)
* 方案 A(推荐):用户在 iOS 内购完成后由客户端上报 `originalTransactionId`,服务端校验通过后“合成一个已支付订单”(`status=2`),触发既有赋权队列
* 方案 B:也可预建“待支付订单”,但 `PurchaseCheckout` 不走网关,只返回“client\_iap”类型,提示客户端用 StoreKit;支付完成后再校验并更新为 `Paid` 入队
1. 状态与权益判定
* 服务端统一以 Apple 校验与通知为准,抽象为 `active/in_grace_period/in_billing_retry/expired/revoked` 并映射到你的订阅与订单状态
## 服务端接口与流程
* `POST /apple/iap/verify`:入参 `originalTransactionId`,校验并创建/更新订单与用户订阅,返回当前权益
* `POST /apple/iap/notifications`:接收 Apple JWS 通知,验签后更新订阅与订单状态(幂等)
* `GET /subscriptions/me`:面向客户端查询当前订阅与权益(聚合 Apple 校验结果)
## 数据模型映射
* 在订单/订阅表补充字段(建议):`Provider=apple_iap``OriginalTransactionId``Environment``ExpiresDate``AutoRenewStatus``InGracePeriod``LastEventType`
* 产品映射:`productIdApp Store ↔ internal subscribeId`,保证同一权益统一计费
## 回调与安全
* JWS 验签:缓存 Apple JWKS 公钥、短生命周期缓存
* App Store Server API:使用 App Store Connect API KeyES256)发起请求,区分生产/沙盒
* 幂等:按 `notificationId/transactionId` 去重
## 测试与上线
* 沙盒测试:购买、续期、失败、宽限期、退款全链路;注意沙盒元数据生效可能需 \~1 小时
* 监控:通知处理失败、验签失败、API 调用异常报警
## 交付物(最小实现)
1. 平台枚举新增 `AppleIAP`
2. `POST /apple/iap/verify``POST /apple/iap/notifications` 路由与逻辑骨架
3. App Store Server API 客户端封装(校验、交易历史、订阅状态)
4. 订单合成与入队赋权打通(复用 `OrderStatusPaid``ProcessTask`
5. 数据表字段扩展与迁移脚本
@@ -1,29 +0,0 @@
# 最后上线时间字段显示修复计划
## 问题分析
用户反馈调用 `curl` 接口后,返回的 JSON 中没有看到“最后上线时间” (`last_login_time`) 字段。
**原因可能如下:**
1. **字段被 `omitempty` 隐藏**: 在 `internal/types/types.go` 中,`LastLoginTime` 字段定义为 `json:"last_login_time,omitempty"`。这意味着如果值为 `0`,该字段在 JSON 序列化时会被忽略,不返回给前端。
2. **数据确实为空**: 用户从未登录过,且没有活跃订阅产生的流量记录,导致计算出的 `LastLoginTime``0`
3. **数据库迁移未生效**: 虽然我们之前修复了迁移文件冲突,但如果数据库中旧的迁移记录未清理或新字段 `last_login_time` 未真正添加成功,会导致数据读取失败(但此时通常会报错,而非字段缺失)。
## 解决方案
为了确保接口始终返回该字段(即使是 0),我们需要移除 `omitempty` 标签,或者确认前端能处理缺失该字段的情况。考虑到用户明确要求“没看到”,建议移除 `omitempty`,让其显式返回 `0` 或时间戳。
同时,我们通过 SQL 检查数据库结构,确保字段已存在。
## 实施步骤
1. **修改 API 定义**:
- 文件: `internal/types/types.go` (及 `apis/types.api` 如果需要重新生成代码,但直接改 go 文件更快捷验证)
- 操作: 将 `LastLoginTime int64 json:"last_login_time,omitempty"` 修改为 `json:"last_login_time"` (移除 `omitempty`)。
- 同理处理 `MemberStatus` 字段。
2. **验证数据库字段**:
- 使用 SQL 工具或日志确认 `user` 表中是否存在 `last_login_time` 列。
3. **验证接口**:
- 再次调用 `curl`,确认即使值为 0 也会返回字段。
## 补充
如果用户是指“有数据但没显示”,那可能是登录逻辑或流量更新逻辑未触发。但首要步骤是让字段显式返回,以便排查是“无数据”还是“字段被隐藏”。
@@ -1,92 +0,0 @@
## 修复目标
* 解决首次设备登录时在 `internal/logic/auth/deviceLoginLogic.go:99``deviceInfo` 赋值导致的空指针崩溃,确保接口稳定返回。
## 根因定位
* 设备不存在分支仅创建用户与设备记录,但未为局部变量 `deviceInfo` 赋值;随后在 `internal/logic/auth/deviceLoginLogic.go:99-100` 使用 `deviceInfo` 导致 `nil` 解引用。
* 参考位置:
* 赋值处:`internal/logic/auth/deviceLoginLogic.go:99-101`
* 设备存在分支赋值:`internal/logic/auth/deviceLoginLogic.go:88-95`
* 设备不存在分支未赋值:`internal/logic/auth/deviceLoginLogic.go:74-79`
* `UpdateDevice` 需要有效设备 `Id``internal/model/user/device.go:58-69`
## 修改方案
1. 在“设备不存在”分支注册完成后,立即通过标识重新查询设备,赋值给 `deviceInfo`
*`internal/logic/auth/deviceLoginLogic.go``if errors.Is(err, gorm.ErrRecordNotFound)` 分支中,`userInfo, err = l.registerUserAndDevice(req)` 之后追加:
* `deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)`
* 如果查询失败则返回数据库查询错误(与现有风格一致)。
2. 在更新设备 UA 前增加空指针保护,并不再忽略更新错误:
*`internal/logic/auth/deviceLoginLogic.go:99-101` 改为:
* 检查 `deviceInfo != nil`
* `deviceInfo.UserAgent = req.UserAgent`
* `if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil {` 记录错误并返回包装后的错误 `xerr.DatabaseUpdateError`
3. 可选优化(减少二次查询):
*`registerUserAndDevice(req)` 的返回值改为 `(*user.User, *user.Device, error)`,在注册时直接返回新建设备对象;调用点随之调整。若选择此方案,仍需在更新前做空指针保护。
## 代码示例(方案1,最小改动)
```go
// internal/logic/auth/deviceLoginLogic.go
// 设备不存在分支注册后追加一次设备查询
userInfo, err = l.registerUserAndDevice(req)
if err != nil {
return nil, err
}
deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
if err != nil {
l.Errorw("query device after register failed",
logger.Field("identifier", req.Identifier),
logger.Field("error", err.Error()),
)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device after register failed: %v", err.Error())
}
// 更新 UA,不忽略更新错误
if deviceInfo != nil {
deviceInfo.UserAgent = req.UserAgent
if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil {
l.Errorw("update device failed",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier),
logger.Field("error", err.Error()),
)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error())
}
}
```
## 测试用例与验证
* 用例1:首次设备标识登录(设备不存在)应成功返回 Token,日志包含注册与登录记录,无 500。
* 用例2:已存在设备标识登录(设备存在)应正常更新 UA 并返回 Token。
* 用例3:模拟数据库异常时应返回一致的业务错误码,不产生 `panic`
## 风险与回滚
* 改动限定在登录逻辑,属最小范围;若出现异常,回滚为当前版本即可。
* 不改变数据结构与外部接口行为,兼容现有客户端。
## 后续优化(可选)
* 统一 `UpdateDevice` 错误处理路径,避免 `_ = ...` 静默失败。
* 为“首次设备登录”场景补充集成测试,保证不再回归。
@@ -1,25 +0,0 @@
# 迁移文件重复问题修复计划
## 问题分析
根据终端日志报错 `panic: failed to init driver with path database: duplicate migration file: 02121_apple_iap_transactions.down.sql`,系统启动失败的原因是存在**重复的迁移版本号**。
`initialize/migrate/database/` 目录下,存在两个版本号相同的迁移文件:
1. `02121_add_user_last_login_time.up.sql` (我们刚刚创建的)
2. `02121_apple_iap_transactions.up.sql` (已存在的)
由于 `golang-migrate` 要求版本号必须唯一,这两个文件都使用了 `02121` 前缀,导致冲突。
## 解决方案
将我们新创建的 `add_user_last_login_time` 迁移文件的版本号递增为 `02122`
## 实施步骤
1. **重命名迁移文件**:
- `02121_add_user_last_login_time.up.sql` -> `02122_add_user_last_login_time.up.sql`
- `02121_add_user_last_login_time.down.sql` -> `02122_add_user_last_login_time.down.sql`
2. **验证**:
- 确认目录下不再有重复前缀的文件。
- 建议用户重新运行程序。
## 补充说明
此操作仅涉及文件重命名,不修改文件内容,风险极低。
-128
View File
@@ -1,128 +0,0 @@
# 设备静默登录机制设计方案
## 需求分析
1. 用户进来之后就是静默登录(设备登录)
2. 用户可以主动关联邮箱;也可以不关联邮箱
3. 用户换手机后在别的地方用邮箱登录会绑定另外一部手机的设备号
4. 没有游客概念,快捷登录进来的用户就是正式用户
## 当前系统分析
### 现有认证流程
- **设备登录**:已存在 `deviceLoginLogic.go`,通过设备标识符自动登录
- **邮箱登录**:已存在 `userLoginLogic.go`,需要邮箱+密码
- **游客模式**:通过 `Order.UserId = 0` 标识游客订单
### 现有设备管理
- **设备绑定**`bindDeviceLogic.go` 处理设备与用户绑定
- **设备解绑**`unbindDeviceLogic.go` 处理设备解绑
- **设备迁移**:支持设备在用户间转移
## 设计方案
### 1. 核心改动策略
- **保留现有设备登录机制**,作为默认登录方式
- **移除游客概念**,所有设备登录用户都是正式用户
- **增强邮箱绑定功能**,支持跨设备登录
- **优化设备迁移逻辑**,支持邮箱登录后绑定新设备
### 2. 具体实现方案
#### 2.1 修改设备登录逻辑
**文件**: `internal/logic/auth/deviceLoginLogic.go`
**改动点**:
- 移除 `registerUserAndDevice` 中的试用激活逻辑
- 确保所有通过设备登录创建的用户都是正式用户
- 保持现有的设备绑定机制
#### 2.2 修改订单处理逻辑
**文件**: `queue/logic/order/activateOrderLogic.go`
**改动点**:
- 移除 `getUserOrCreate` 中的游客判断逻辑 (`orderInfo.UserId == 0`)
- 移除 `createGuestUser` 函数
- 修改为:如果订单没有关联用户,通过设备标识符创建或获取用户
#### 2.3 修改订单关闭逻辑
**文件**: `internal/logic/public/order/closeOrderLogic.go`
**改动点**:
- 移除对 `UserId == 0` 的特殊处理
- 统一处理所有订单的关闭逻辑
#### 2.4 增强邮箱登录逻辑
**文件**: `internal/logic/auth/userLoginLogic.go`
**改动点**:
- 在邮箱登录成功后,如果提供了设备标识符,自动绑定设备
- 支持邮箱登录后在新设备上的自动绑定
#### 2.5 优化设备绑定逻辑
**文件**: `internal/logic/auth/bindDeviceLogic.go`
**改动点**:
- 增强设备迁移逻辑,支持邮箱用户登录新设备时的自动绑定
- 保持现有的设备冲突处理机制
### 3. 数据库变更
**无需修改数据库结构**,现有的用户和设备表结构已经支持新的需求。
### 4. API 变更
**无需修改 API 接口**,现有的设备登录和邮箱登录接口已经满足需求。
### 5. 前端适配
**前端需要调整**:
- 默认使用设备登录作为主要登录方式
- 提供邮箱绑定入口
- 在新设备上提供邮箱登录选项
## 实施步骤
### 第一步:修改订单逻辑
1. 修改 `activateOrderLogic.go`,移除游客概念
2. 修改 `closeOrderLogic.go`,统一订单处理逻辑
### 第二步:增强设备登录
1. 确保设备登录创建的都是正式用户
2. 优化设备绑定逻辑
### 第三步:增强邮箱登录
1. 在邮箱登录后支持设备绑定
2. 优化跨设备登录体验
### 第四步:测试验证
1. 测试设备静默登录
2. 测试邮箱绑定功能
3. 测试跨设备登录
## 优势分析
### 1. 最小化改动
- 复用现有的设备登录机制
- 保持现有的数据库结构
- 保持现有的 API 接口
### 2. 用户体验提升
- 用户进入即可使用,无需注册
- 支持邮箱绑定,便于跨设备使用
- 保持数据连续性
### 3. 系统稳定性
- 基于现有成熟机制
- 减少新增代码量
- 降低引入 bug 的风险
## 风险评估
### 1. 数据迁移
- **风险**: 现有游客数据需要处理
- **方案**: 可以保持现有游客数据不变,新用户使用新机制
### 2. 兼容性
- **风险**: 现有客户端可能需要适配
- **方案**: 保持 API 兼容,逐步引导用户使用新机制
### 3. 性能影响
- **风险**: 设备登录可能增加数据库压力
- **方案**: 现有机制已经过验证,影响可控
+3 -2
View File
@@ -24,11 +24,11 @@ RUN BUILD_TIME=$(date -u +"%Y-%m-%d %H:%M:%S") && \
go build -ldflags="-s -w -X 'github.com/perfect-panel/server/pkg/constant.Version=${VERSION}' -X 'github.com/perfect-panel/server/pkg/constant.BuildTime=${BUILD_TIME}'" -o /app/ppanel ppanel.go
# Final minimal image
FROM alpine:latest
FROM scratch
# Copy CA certificates and timezone data
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
ENV TZ=Asia/Shanghai
@@ -36,6 +36,7 @@ ENV TZ=Asia/Shanghai
WORKDIR /app
COPY --from=builder /app/ppanel /app/ppanel
COPY --from=builder /build/etc /app/etc
# Expose the port (optional)
EXPOSE 8080
View File
-111
View File
@@ -1,111 +0,0 @@
server {
listen 80;
server_name hifastapp.com www.hifastapp.com www.hifastvpn.com hifastvpn.com hifast.biz www.hifast.biz;
location ^~ /.well-known/acme-challenge/ {
root /etc/letsencrypt;
}
# 统一 HTTP 转 HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name hifastvpn.com www.hifastvpn.com;
ssl_certificate /etc/letsencrypt/live/hifastvpn.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/hifastvpn.com/privkey.pem; # managed by Certbot
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
root /var/www/down;
index index.html index.htm;
location /api/ {
proxy_pass https://api.hifast.biz/;
proxy_set_header Host api.hifast.biz;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location ^~ /.well-known/acme-challenge/ {
root /etc/letsencrypt;
}
location / {
try_files $uri $uri/ /index.html;
}
location /download/ {
autoindex_exact_size off;
autoindex_localtime on;
}
}
server {
listen 443 ssl http2;
server_name hifastapp.com www.hifastapp.com;
# 使用 -0001 的新证书(通常包含 www)
ssl_certificate /etc/letsencrypt/live/hifastapp.com-0001/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/hifastapp.com-0001/privkey.pem; # managed by Certbot
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
root /var/www/down;
index index.html index.htm;
location /api/ {
proxy_pass https://api.hifast.biz/;
proxy_set_header Host api.hifast.biz;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location ^~ /.well-known/acme-challenge/ {
root /etc/letsencrypt;
}
location / {
try_files $uri $uri/ /index.html;
}
location /download/ {
autoindex_exact_size off;
autoindex_localtime on;
}
}
server {
listen 443 ssl http2;
server_name hifast.biz www.hifast.biz;
ssl_certificate /etc/letsencrypt/live/hifast.biz/hifast.biz.cer;
ssl_certificate_key /etc/letsencrypt/live/hifast.biz/hifast.biz.key;
root /var/www/lp;
index index.html index.htm;
location ^~ /.well-known/acme-challenge/ {
root /etc/letsencrypt;
}
location / {
try_files $uri $uri/ /index.html;
}
}
+67 -51
View File
@@ -8,16 +8,24 @@ import (
)
type Adapter struct {
SiteName string // 站点名称
Servers []*node.Node // 服务器列表
UserInfo User // 用户信息
ClientTemplate string // 客户端配置模板
OutputFormat string // 输出格式,默认是 base64
SubscribeName string // 订阅名称
Type string // 协议类型
SiteName string // 站点名称
Servers []*node.Node // 服务器列表
UserInfo User // 用户信息
ClientTemplate string // 客户端配置模板
OutputFormat string // 输出格式,默认是 base64
SubscribeName string // 订阅名称
Params map[string]string // 其他参数
}
type Option func(*Adapter)
func WithParams(params map[string]string) Option {
return func(opts *Adapter) {
opts.Params = params
}
}
// WithServers 设置服务器列表
func WithServers(servers []*node.Node) Option {
return func(opts *Adapter) {
@@ -76,6 +84,7 @@ func (adapter *Adapter) Client() (*Client, error) {
OutputFormat: adapter.OutputFormat,
Proxies: []Proxy{},
UserInfo: adapter.UserInfo,
Params: adapter.Params,
}
proxies, err := adapter.Proxies(adapter.Servers)
@@ -101,51 +110,58 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) {
}
for _, protocol := range protocols {
if protocol.Type == item.Protocol {
proxies = append(proxies, Proxy{
Sort: item.Sort,
Name: item.Name,
Server: item.Address,
Port: item.Port,
Type: item.Protocol,
Tags: strings.Split(item.Tags, ","),
Security: protocol.Security,
SNI: protocol.SNI,
AllowInsecure: protocol.AllowInsecure,
Fingerprint: protocol.Fingerprint,
RealityServerAddr: protocol.RealityServerAddr,
RealityServerPort: protocol.RealityServerPort,
RealityPrivateKey: protocol.RealityPrivateKey,
RealityPublicKey: protocol.RealityPublicKey,
RealityShortId: protocol.RealityShortId,
Transport: protocol.Transport,
Host: protocol.Host,
Path: protocol.Path,
ServiceName: protocol.ServiceName,
Method: protocol.Cipher,
ServerKey: protocol.ServerKey,
Flow: protocol.Flow,
HopPorts: protocol.HopPorts,
HopInterval: protocol.HopInterval,
ObfsPassword: protocol.ObfsPassword,
DisableSNI: protocol.DisableSNI,
ReduceRtt: protocol.ReduceRtt,
UDPRelayMode: protocol.UDPRelayMode,
CongestionController: protocol.CongestionController,
UpMbps: protocol.UpMbps,
DownMbps: protocol.DownMbps,
PaddingScheme: protocol.PaddingScheme,
Multiplex: protocol.Multiplex,
XhttpMode: protocol.XhttpMode,
XhttpExtra: protocol.XhttpExtra,
Encryption: protocol.Encryption,
EncryptionMode: protocol.EncryptionMode,
EncryptionRtt: protocol.EncryptionRtt,
EncryptionTicket: protocol.EncryptionTicket,
EncryptionServerPadding: protocol.EncryptionServerPadding,
EncryptionPrivateKey: protocol.EncryptionPrivateKey,
EncryptionClientPadding: protocol.EncryptionClientPadding,
EncryptionPassword: protocol.EncryptionPassword,
})
proxies = append(
proxies,
Proxy{
Sort: item.Sort,
Name: item.Name,
Server: item.Address,
Port: item.Port,
Type: item.Protocol,
Tags: strings.Split(item.Tags, ","),
Security: protocol.Security,
SNI: protocol.SNI,
AllowInsecure: protocol.AllowInsecure,
Fingerprint: protocol.Fingerprint,
RealityServerAddr: protocol.RealityServerAddr,
RealityServerPort: protocol.RealityServerPort,
RealityPrivateKey: protocol.RealityPrivateKey,
RealityPublicKey: protocol.RealityPublicKey,
RealityShortId: protocol.RealityShortId,
Transport: protocol.Transport,
Host: protocol.Host,
Path: protocol.Path,
ServiceName: protocol.ServiceName,
Method: protocol.Cipher,
ServerKey: protocol.ServerKey,
Flow: protocol.Flow,
HopPorts: protocol.HopPorts,
HopInterval: protocol.HopInterval,
ObfsPassword: protocol.ObfsPassword,
UpMbps: protocol.UpMbps,
DownMbps: protocol.DownMbps,
DisableSNI: protocol.DisableSNI,
ReduceRtt: protocol.ReduceRtt,
UDPRelayMode: protocol.UDPRelayMode,
CongestionController: protocol.CongestionController,
PaddingScheme: protocol.PaddingScheme,
Multiplex: protocol.Multiplex,
XhttpMode: protocol.XhttpMode,
XhttpExtra: protocol.XhttpExtra,
Encryption: protocol.Encryption,
EncryptionMode: protocol.EncryptionMode,
EncryptionRtt: protocol.EncryptionRtt,
EncryptionTicket: protocol.EncryptionTicket,
EncryptionServerPadding: protocol.EncryptionServerPadding,
EncryptionPrivateKey: protocol.EncryptionPrivateKey,
EncryptionClientPadding: protocol.EncryptionClientPadding,
EncryptionPassword: protocol.EncryptionPassword,
Ratio: protocol.Ratio,
CertMode: protocol.CertMode,
CertDNSProvider: protocol.CertDNSProvider,
CertDNSEnv: protocol.CertDNSEnv,
},
)
}
}
}
+8 -6
View File
@@ -93,12 +93,13 @@ type User struct {
}
type Client struct {
SiteName string // Name of the site
SubscribeName string // Name of the subscription
ClientTemplate string // Template for the entire client configuration
OutputFormat string // json, yaml, etc.
Proxies []Proxy // List of proxy configurations
UserInfo User // User information
SiteName string // Name of the site
SubscribeName string // Name of the subscription
ClientTemplate string // Template for the entire client configuration
OutputFormat string // json, yaml, etc.
Proxies []Proxy // List of proxy configurations
UserInfo User // User information
Params map[string]string // Additional parameters
}
func (c *Client) Build() ([]byte, error) {
@@ -119,6 +120,7 @@ func (c *Client) Build() ([]byte, error) {
"OutputFormat": c.OutputFormat,
"Proxies": proxies,
"UserInfo": c.UserInfo,
"Params": c.Params,
})
if err != nil {
return nil, err
+7 -7
View File
@@ -14,14 +14,14 @@ type (
CreateOrderRequest {
UserId int64 `json:"user_id" validate:"required"`
Type uint8 `json:"type" validate:"required"`
Quantity int64 `json:"quantity,omitempty"`
Price int64 `json:"price" validate:"required"`
Amount int64 `json:"amount" validate:"required"`
Discount int64 `json:"discount,omitempty"`
Quantity int64 `json:"quantity,omitempty" validate:"omitempty,lte=1000"`
Price int64 `json:"price" validate:"required,gte=0,lte=2000000000"`
Amount int64 `json:"amount" validate:"required,gte=0,lte=2147483647"`
Discount int64 `json:"discount,omitempty" validate:"omitempty,gte=0,lte=2000000000"`
Coupon string `json:"coupon,omitempty"`
CouponDiscount int64 `json:"coupon_discount,omitempty"`
Commission int64 `json:"commission"`
FeeAmount int64 `json:"fee_amount" validate:"required"`
CouponDiscount int64 `json:"coupon_discount,omitempty" validate:"omitempty,gte=0,lte=2000000000"`
Commission int64 `json:"commission" validate:"gte=0,lte=2000000000"`
FeeAmount int64 `json:"fee_amount" validate:"required,gte=0,lte=2000000000"`
PaymentId int64 `json:"payment_id" validate:"required"`
TradeNo string `json:"trade_no,omitempty"`
Status uint8 `json:"status,omitempty"`
+95
View File
@@ -0,0 +1,95 @@
syntax = "v1"
info (
title: "redemption API"
desc: "API for redemption code management"
author: "Tension"
email: "tension@ppanel.com"
version: "0.0.1"
)
import "../types.api"
type (
CreateRedemptionCodeRequest {
TotalCount int64 `json:"total_count" validate:"required"`
SubscribePlan int64 `json:"subscribe_plan" validate:"required"`
UnitTime string `json:"unit_time" validate:"required,oneof=day month quarter half_year year"`
Quantity int64 `json:"quantity" validate:"required"`
BatchCount int64 `json:"batch_count" validate:"required,min=1"`
}
UpdateRedemptionCodeRequest {
Id int64 `json:"id" validate:"required"`
TotalCount int64 `json:"total_count,omitempty"`
SubscribePlan int64 `json:"subscribe_plan,omitempty"`
UnitTime string `json:"unit_time,omitempty" validate:"omitempty,oneof=day month quarter half_year year"`
Quantity int64 `json:"quantity,omitempty"`
Status int64 `json:"status,omitempty" validate:"omitempty,oneof=0 1"`
}
ToggleRedemptionCodeStatusRequest {
Id int64 `json:"id" validate:"required"`
Status int64 `json:"status" validate:"oneof=0 1"`
}
DeleteRedemptionCodeRequest {
Id int64 `json:"id" validate:"required"`
}
BatchDeleteRedemptionCodeRequest {
Ids []int64 `json:"ids" validate:"required"`
}
GetRedemptionCodeListRequest {
Page int64 `form:"page" validate:"required"`
Size int64 `form:"size" validate:"required"`
SubscribePlan int64 `form:"subscribe_plan,omitempty"`
UnitTime string `form:"unit_time,omitempty"`
Code string `form:"code,omitempty"`
}
GetRedemptionCodeListResponse {
Total int64 `json:"total"`
List []RedemptionCode `json:"list"`
}
GetRedemptionRecordListRequest {
Page int64 `form:"page" validate:"required"`
Size int64 `form:"size" validate:"required"`
UserId int64 `form:"user_id,omitempty"`
CodeId int64 `form:"code_id,omitempty"`
}
GetRedemptionRecordListResponse {
Total int64 `json:"total"`
List []RedemptionRecord `json:"list"`
}
)
@server (
prefix: v1/admin/redemption
group: admin/redemption
middleware: AuthMiddleware
)
service ppanel {
@doc "Create redemption code"
@handler CreateRedemptionCode
post /code (CreateRedemptionCodeRequest)
@doc "Update redemption code"
@handler UpdateRedemptionCode
put /code (UpdateRedemptionCodeRequest)
@doc "Toggle redemption code status"
@handler ToggleRedemptionCodeStatus
put /code/status (ToggleRedemptionCodeStatusRequest)
@doc "Delete redemption code"
@handler DeleteRedemptionCode
delete /code (DeleteRedemptionCodeRequest)
@doc "Batch delete redemption code"
@handler BatchDeleteRedemptionCode
delete /code/batch (BatchDeleteRedemptionCodeRequest)
@doc "Get redemption code list"
@handler GetRedemptionCodeList
get /code/list (GetRedemptionCodeListRequest) returns (GetRedemptionCodeListResponse)
@doc "Get redemption record list"
@handler GetRedemptionRecordList
get /record/list (GetRedemptionRecordListRequest) returns (GetRedemptionRecordListResponse)
}
-8
View File
@@ -189,14 +189,6 @@ service ppanel {
@handler ToggleNodeStatus
post /node/status/toggle (ToggleNodeStatusRequest)
@doc "Check if there is any server or node to migrate"
@handler HasMigrateSeverNode
get /migrate/has returns (HasMigrateSeverNodeResponse)
@doc "Migrate server and node data to new database"
@handler MigrateServerNode
post /migrate/run returns (MigrateServerNodeResponse)
@doc "Reset server sort"
@handler ResetSortWithServer
post /server/sort (ResetSortRequest)
+51 -42
View File
@@ -34,50 +34,52 @@ type (
Ids []int64 `json:"ids" validate:"required"`
}
CreateSubscribeRequest {
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
ShowOriginalPrice bool `json:"show_original_price"`
}
UpdateSubscribeRequest {
Id int64 `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
Id int64 `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show *bool `json:"show"`
Sell *bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction *bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset *bool `json:"renewal_reset"`
ShowOriginalPrice bool `json:"show_original_price"`
}
SubscribeSortRequest {
Sort []SortItem `json:"sort"`
@@ -102,6 +104,9 @@ type (
BatchDeleteSubscribeRequest {
Ids []int64 `json:"ids" validate:"required"`
}
ResetAllSubscribeTokenResponse {
Success bool `json:"success"`
}
)
@server (
@@ -157,5 +162,9 @@ service ppanel {
@doc "Subscribe sort"
@handler SubscribeSort
post /sort (SubscribeSortRequest)
@doc "Reset all subscribe tokens"
@handler ResetAllSubscribeToken
post /reset_all_token returns (ResetAllSubscribeTokenResponse)
}
+9
View File
@@ -22,6 +22,11 @@ type (
CurrentTime string `json:"current_time"`
Ratio float32 `json:"ratio"`
}
ModuleConfig {
Secret string `json:"secret"` // 通讯密钥
ServiceName string `json:"service_name"` // 服务名称
ServiceVersion string `json:"service_version"` // 服务版本
}
)
@server (
@@ -125,5 +130,9 @@ service ppanel {
@doc "PreView Node Multiplier"
@handler PreViewNodeMultiplier
get /node_multiplier/preview returns (PreViewNodeMultiplierResponse)
@doc "Get Module Config"
@handler GetModuleConfig
get /module returns (ModuleConfig)
}
+12
View File
@@ -17,6 +17,14 @@ type (
VersionResponse {
Version string `json:"version"`
}
QueryIPLocationRequest {
IP string `form:"ip" validate:"required"`
}
QueryIPLocationResponse {
Country string `json:"country"`
Region string `json:"region,omitempty"`
City string `json:"city"`
}
)
@server (
@@ -36,5 +44,9 @@ service ppanel {
@doc "Get Version"
@handler GetVersion
get /version returns (VersionResponse)
@doc "Query IP Location"
@handler QueryIPLocation
get /ip/location (QueryIPLocationRequest) returns (QueryIPLocationResponse)
}
+22 -2
View File
@@ -19,9 +19,10 @@ type (
Size int `form:"size"`
Search string `form:"search,omitempty"`
UserId *int64 `form:"user_id,omitempty"`
Unscoped bool `form:"unscoped,omitempty"`
SubscribeId *int64 `form:"subscribe_id,omitempty"`
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
DeviceId string `form:"device_id,omitempty"`
ShortCode string `form:"short_code,omitempty"`
}
// GetUserListResponse
GetUserListResponse {
@@ -179,11 +180,17 @@ type (
Total int64 `json:"total"`
}
DeleteUserSubscribeRequest {
UserSubscribeId int64 `json:"user_subscribe_id"`
UserSubscribeId int64 `json:"user_subscribe_id,string"`
}
GetUserSubscribeByIdRequest {
Id int64 `form:"id" validate:"required"`
}
ToggleUserSubscribeStatusRequest {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
ResetUserSubscribeTrafficRequest {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
)
@server (
@@ -292,4 +299,17 @@ service ppanel {
@doc "Get user login logs"
@handler GetUserLoginLogs
get /login/logs (GetUserLoginLogsRequest) returns (GetUserLoginLogsResponse)
@doc "Reset user subscribe token"
@handler ResetUserSubscribeToken
post /subscribe/reset/token (ResetUserSubscribeTokenRequest)
@doc "Stop user subscribe"
@handler ToggleUserSubscribeStatus
post /subscribe/toggle (ToggleUserSubscribeStatusRequest)
@doc "Reset user subscribe traffic"
@handler ResetUserSubscribeTraffic
post /subscribe/reset/traffic (ResetUserSubscribeTrafficRequest)
}
+1 -14
View File
@@ -50,16 +50,6 @@ type (
LoginType string `header:"Login-Type"`
CfToken string `json:"cf_token,optional"`
}
EmailLoginRequest {
Identifier string `json:"identifier"`
Email string `json:"email" validate:"required"`
Code string `json:"code" validate:"required"`
Invite string `json:"invite,optional"`
IP string `header:"X-Original-Forwarded-For"`
UserAgent string `header:"User-Agent"`
LoginType string `header:"Login-Type"`
CfToken string `json:"cf_token,optional"`
}
LoginResponse {
Token string `json:"token"`
}
@@ -134,6 +124,7 @@ type (
IP string `header:"X-Original-Forwarded-For"`
UserAgent string `json:"user_agent" validate:"required"`
CfToken string `json:"cf_token,optional"`
ShortCode string `json:"short_code,optional"`
}
)
@@ -151,10 +142,6 @@ service ppanel {
@handler CheckUser
get /check (CheckUserRequest) returns (CheckUserResponse)
@doc "Email Login"
@handler EmailLogin
post /login/email (EmailLoginRequest) returns (LoginResponse)
@doc "User register"
@handler UserRegister
post /register (UserRegisterRequest) returns (LoginResponse)
+9 -9
View File
@@ -87,11 +87,10 @@ type (
Total int64 `json:"total"`
List []SubscribeClient `json:"list"`
}
ContactRequest {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
OtherContact string `json:"other_contact,optional"`
Notes string `json:"notes,optional"`
HeartbeatResponse {
Status bool `json:"status"`
Message string `json:"message,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}
)
@@ -105,10 +104,6 @@ service ppanel {
@handler GetGlobalConfig
get /site/config returns (GetGlobalConfigResponse)
@doc "Submit contact info"
@handler SubmitContact
post /contact (ContactRequest)
@doc "Get Tos Content"
@handler GetTos
get /site/tos returns (GetTosResponse)
@@ -140,4 +135,9 @@ service ppanel {
@doc "Get Client"
@handler GetClient
get /client returns (GetSubscribeClientResponse)
@doc "Heartbeat"
@handler Heartbeat
get /heartbeat returns (HeartbeatResponse)
}
+32
View File
@@ -0,0 +1,32 @@
syntax = "v1"
info (
title: "redemption API"
desc: "API for redemption"
author: "Tension"
email: "tension@ppanel.com"
version: "0.0.1"
)
import "../types.api"
type (
RedeemCodeRequest {
Code string `json:"code" validate:"required"`
}
RedeemCodeResponse {
Message string `json:"message"`
}
)
@server (
prefix: v1/public/redemption
group: public/redemption
jwt: JwtAuth
middleware: AuthMiddleware,DeviceMiddleware
)
service ppanel {
@doc "Redeem code"
@handler RedeemCode
post / (RedeemCodeRequest) returns (RedeemCodeResponse)
}
+42 -39
View File
@@ -14,45 +14,48 @@ type (
QuerySubscribeListRequest {
Language string `form:"language"`
}
QueryUserSubscribeNodeListResponse {
List []UserSubscribeInfo `json:"list"`
}
UserSubscribeInfo {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`
OrderId int64 `json:"order_id"`
SubscribeId int64 `json:"subscribe_id"`
StartTime int64 `json:"start_time"`
ExpireTime int64 `json:"expire_time"`
FinishedAt int64 `json:"finished_at"`
ResetTime int64 `json:"reset_time"`
Traffic int64 `json:"traffic"`
Download int64 `json:"download"`
Upload int64 `json:"upload"`
Token string `json:"token"`
Status uint8 `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
IsTryOut bool `json:"is_try_out"`
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
}
UserSubscribeNodeInfo {
Id int64 `json:"id"`
Name string `json:"name"`
Uuid string `json:"uuid"`
Protocol string `json:"protocol"`
Protocols string `json:"protocols"`
Port uint16 `json:"port"`
Address string `json:"address"`
Tags []string `json:"tags"`
Country string `json:"country"`
City string `json:"city"`
Longitude string `json:"longitude"`
QueryUserSubscribeNodeListResponse {
List []UserSubscribeInfo `json:"list"`
}
UserSubscribeInfo {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`
OrderId int64 `json:"order_id"`
SubscribeId int64 `json:"subscribe_id"`
StartTime int64 `json:"start_time"`
ExpireTime int64 `json:"expire_time"`
FinishedAt int64 `json:"finished_at"`
ResetTime int64 `json:"reset_time"`
Traffic int64 `json:"traffic"`
Download int64 `json:"download"`
Upload int64 `json:"upload"`
Token string `json:"token"`
Status uint8 `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
IsTryOut bool `json:"is_try_out"`
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
}
UserSubscribeNodeInfo{
Id int64 `json:"id"`
Name string `json:"name"`
Uuid string `json:"uuid"`
Protocol string `json:"protocol"`
Protocols string `json:"protocols"`
Port uint16 `json:"port"`
Address string `json:"address"`
Tags []string `json:"tags"`
Country string `json:"country"`
City string `json:"city"`
Longitude string `json:"longitude"`
Latitude string `json:"latitude"`
LatitudeCenter string `json:"latitude_center"`
LongitudeCenter string `json:"longitude_center"`
CreatedAt int64 `json:"created_at"`
}
CreatedAt int64 `json:"created_at"`
}
)
@server (
@@ -65,8 +68,8 @@ service ppanel {
@handler QuerySubscribeList
get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse)
@doc "Get user subscribe node info"
@handler QueryUserSubscribeNodeList
get /node/list returns (QueryUserSubscribeNodeListResponse)
@doc "Get user subscribe node info"
@handler QueryUserSubscribeNodeList
get /node/list returns (QueryUserSubscribeNodeListResponse)
}
+92 -27
View File
@@ -66,9 +66,7 @@ type (
UnbindOAuthRequest {
Method string `json:"method"`
}
ResetUserSubscribeTokenRequest {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
GetLoginLogRequest {
Page int `form:"page"`
Size int `form:"size"`
@@ -97,27 +95,63 @@ type (
Email string `json:"email" validate:"required"`
Code string `json:"code" validate:"required"`
}
BindEmailWithVerificationRequest {
Email string `json:"email" validate:"required"`
Code string `json:"code" validate:"required"`
GetDeviceListResponse {
List []UserDevice `json:"list"`
Total int64 `json:"total"`
}
UnbindDeviceRequest {
Id int64 `json:"id" validate:"required"`
}
UpdateUserSubscribeNoteRequest {
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
Note string `json:"note" validate:"max=500"`
}
BindEmailWithVerificationResponse {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Token string `json:"token,omitempty"` // 设备关联后的新Token
UserId int64 `json:"user_id,omitempty"` // 目标用户ID
UpdateUserRulesRequest {
Rules []string `json:"rules" validate:"required"`
}
GetDeviceListResponse {
List []UserDevice `json:"list"`
Total int64 `json:"total"`
CommissionWithdrawRequest {
Amount int64 `json:"amount"`
Content string `json:"content"`
}
UnbindDeviceRequest {
Id int64 `json:"id" validate:"required"`
WithdrawalLog {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`
Amount int64 `json:"amount"`
Content string `json:"content"`
Status uint8 `json:"status"`
Reason string `json:"reason,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
GetSubscribeStatusResponse {
DeviceStatus bool `json:"device_status"`
EmailStatus bool `json:"email_status"`
QueryWithdrawalLogListRequest {
Page int `form:"page"`
Size int `form:"size"`
}
QueryWithdrawalLogListResponse {
List []WithdrawalLog `json:"list"`
Total int64 `json:"total"`
}
GetDeviceOnlineStatsResponse {
WeeklyStats []WeeklyStat `json:"weekly_stats"`
ConnectionRecords ConnectionRecords `json:"connection_records"`
}
WeeklyStat {
Day int `json:"day"`
DayName string `json:"day_name"`
Hours float64 `json:"hours"`
}
ConnectionRecords {
CurrentContinuousDays int64 `json:"current_continuous_days"`
HistoryContinuousDays int64 `json:"history_continuous_days"`
LongestSingleConnection int64 `json:"longest_single_connection"`
}
)
@server (
@@ -202,10 +236,6 @@ service ppanel {
@handler GetSubscribeLog
get /subscribe_log (GetSubscribeLogRequest) returns (GetSubscribeLogResponse)
@doc "Get Subscribe Status (device/email)"
@handler GetSubscribeStatus
get /subscribe_status returns (GetSubscribeStatusResponse)
@doc "Verify Email"
@handler VerifyEmail
post /verify_email (VerifyEmailRequest)
@@ -218,12 +248,47 @@ service ppanel {
@handler UpdateBindEmail
put /bind_email (UpdateBindEmailRequest)
@doc "Get Device List"
@handler GetDeviceList
get /devices returns (GetDeviceListResponse)
@doc "Get Device List"
@handler GetDeviceList
get /devices returns (GetDeviceListResponse)
@doc "Unbind Device"
@handler UnbindDevice
put /unbind_device (UnbindDeviceRequest)
}
@doc "Update User Subscribe Note"
@handler UpdateUserSubscribeNote
put /subscribe_note (UpdateUserSubscribeNoteRequest)
@doc "Update User Rules"
@handler UpdateUserRules
put /rules (UpdateUserRulesRequest)
@doc "Commission Withdraw"
@handler CommissionWithdraw
post /commission_withdraw (CommissionWithdrawRequest) returns (WithdrawalLog)
@doc "Query Withdrawal Log"
@handler QueryWithdrawalLog
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
@doc "Device Online Statistics"
@handler DeviceOnlineStatistics
get /device_online_statistics returns (GetDeviceOnlineStatsResponse)
@doc "Delete Current User Account"
@handler DeleteCurrentUserAccount
delete /current_user_account
}
@server(
prefix: v1/public/user
group: public/user/ws
middleware: AuthMiddleware
)
service ppanel {
@doc "Webosocket Device Connect"
@handler DeviceWsConnect
get /device_ws_connect
}
+61 -34
View File
@@ -26,10 +26,9 @@ type (
EnableLoginNotify bool `json:"enable_login_notify"`
EnableSubscribeNotify bool `json:"enable_subscribe_notify"`
EnableTradeNotify bool `json:"enable_trade_notify"`
LastLoginTime int64 `json:"last_login_time"`
MemberStatus string `json:"member_status"`
AuthMethods []UserAuthMethod `json:"auth_methods"`
UserDevices []UserDevice `json:"user_devices"`
Rules []string `json:"rules"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
DeletedAt int64 `json:"deleted_at,omitempty"`
@@ -152,6 +151,7 @@ type (
EnableIpRegisterLimit bool `json:"enable_ip_register_limit"`
IpRegisterLimit int64 `json:"ip_register_limit"`
IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"`
DeviceLimit int64 `json:"device_limit"`
}
VerifyConfig {
TurnstileSiteKey string `json:"turnstile_site_key"`
@@ -207,33 +207,34 @@ type (
}
SubscribeDiscount {
Quantity int64 `json:"quantity"`
Discount int64 `json:"discount"`
Discount float64 `json:"discount"`
}
Subscribe {
Id int64 `json:"id"`
Name string `json:"name"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show bool `json:"show"`
Sell bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset bool `json:"renewal_reset"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
Id int64 `json:"id"`
Name string `json:"name"`
Language string `json:"language"`
Description string `json:"description"`
UnitPrice int64 `json:"unit_price"`
UnitTime string `json:"unit_time"`
Discount []SubscribeDiscount `json:"discount"`
Replacement int64 `json:"replacement"`
Inventory int64 `json:"inventory"`
Traffic int64 `json:"traffic"`
SpeedLimit int64 `json:"speed_limit"`
DeviceLimit int64 `json:"device_limit"`
Quota int64 `json:"quota"`
Nodes []int64 `json:"nodes"`
NodeTags []string `json:"node_tags"`
Show bool `json:"show"`
Sell bool `json:"sell"`
Sort int64 `json:"sort"`
DeductionRatio int64 `json:"deduction_ratio"`
AllowDeduction bool `json:"allow_deduction"`
ResetCycle int64 `json:"reset_cycle"`
RenewalReset bool `json:"renewal_reset"`
ShowOriginalPrice bool `json:"show_original_price"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
SubscribeGroup {
Id int64 `json:"id"`
@@ -447,6 +448,28 @@ type (
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
RedemptionCode {
Id int64 `json:"id"`
Code string `json:"code"`
TotalCount int64 `json:"total_count"`
UsedCount int64 `json:"used_count"`
SubscribePlan int64 `json:"subscribe_plan"`
UnitTime string `json:"unit_time"`
Quantity int64 `json:"quantity"`
Status int64 `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
RedemptionRecord {
Id int64 `json:"id"`
RedemptionCodeId int64 `json:"redemption_code_id"`
UserId int64 `json:"user_id"`
SubscribeId int64 `json:"subscribe_id"`
UnitTime string `json:"unit_time"`
Quantity int64 `json:"quantity"`
RedeemedAt int64 `json:"redeemed_at"`
CreatedAt int64 `json:"created_at"`
}
Announcement {
Id int64 `json:"id"`
Title string `json:"title"`
@@ -472,6 +495,7 @@ type (
Upload int64 `json:"upload"`
Token string `json:"token"`
Status uint8 `json:"status"`
Short string `json:"short"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
@@ -580,7 +604,7 @@ type (
//public order
PurchaseOrderRequest {
SubscribeId int64 `json:"subscribe_id"`
Quantity int64 `json:"quantity" validate:"required,gt=0"`
Quantity int64 `json:"quantity" validate:"required,gt=0,lte=1000"`
Payment int64 `json:"payment,omitempty"`
Coupon string `json:"coupon,omitempty"`
}
@@ -598,7 +622,7 @@ type (
}
RenewalOrderRequest {
UserSubscribeID int64 `json:"user_subscribe_id"`
Quantity int64 `json:"quantity"`
Quantity int64 `json:"quantity" validate:"lte=1000"`
Payment int64 `json:"payment"`
Coupon string `json:"coupon,omitempty"`
}
@@ -613,7 +637,7 @@ type (
OrderNo string `json:"order_no"`
}
RechargeOrderRequest {
Amount int64 `json:"amount"`
Amount int64 `json:"amount" validate:"required,gt=0,lte=2000000000"`
Payment int64 `json:"payment"`
}
RechargeOrderResponse {
@@ -634,10 +658,8 @@ type (
PublishableKey string `json:"publishable_key"`
}
QueryOrderListRequest {
Page int `form:"page" validate:"required"`
Size int `form:"size" validate:"required"`
Status uint8 `form:"status,omitempty"`
Search string `form:"search,omitempty"`
Page int `form:"page" validate:"required"`
Size int `form:"size" validate:"required"`
}
QueryOrderListResponse {
Total int64 `json:"total"`
@@ -675,6 +697,7 @@ type (
List []SubscribeGroup `json:"list"`
Total int64 `json:"total"`
}
GetUserSubscribeTrafficLogsRequest {
Page int `form:"page"`
Size int `form:"size"`
@@ -847,5 +870,9 @@ type (
CertDNSProvider string `json:"cert_dns_provider,omitempty"` // DNS provider for certificate
CertDNSEnv string `json:"cert_dns_env,omitempty"` // Environment for DNS provider
}
// reset user subscribe token
ResetUserSubscribeTokenRequest {
UserSubscribeId int64 `json:"user_subscribe_id"`
}
)
-4
View File
@@ -1,4 +0,0 @@
SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build -o ppanel .\ppanel.go
Vendored Executable → Regular
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 MiB

After

Width:  |  Height:  |  Size: 61 MiB

-5
View File
@@ -23,7 +23,6 @@ import (
"github.com/perfect-panel/server/pkg/orm"
"github.com/perfect-panel/server/pkg/service"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/trace"
"github.com/perfect-panel/server/queue"
"github.com/perfect-panel/server/scheduler"
"github.com/spf13/cobra"
@@ -50,7 +49,6 @@ var startCmd = &cobra.Command{
func run() {
services := getServers()
defer services.Stop()
defer trace.StopAgent()
go services.Start()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
@@ -91,9 +89,6 @@ func getServers() *service.Group {
logger.Errorf("Logger setup failed: %v", err.Error())
}
// init trace
trace.StartAgent(c.Trace)
// init service context
ctx := svc.NewServiceContext(c)
services := service.NewServiceGroup()
-171
View File
@@ -1,171 +0,0 @@
package main
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"time"
)
// 配置区域 - 请在此处填入您的真实信息进行测试
const (
// 必填:您的 Key ID (从 App Store Connect 获取)
KeyID = "2C4X3HVPM8"
// 必填:您的 Issuer ID (从 App Store Connect 获取,通常是一个 UUID)
IssuerID = "34f54810-5118-4b7f-8069-c8c1e012b7a9" // 请替换为您真实的 Issuer ID
// 必填:您的 Bundle ID (App 的包名)
BundleID = "com.taw.hifastvpn" // 请替换为您真实的 Bundle ID
// 必填:用于测试的 Transaction ID (任意一个真实的交易 ID)
TestTransactionID = "2000001083318819"
// 必填:是否为沙盒环境
IsSandbox = true
)
// P8 私钥内容 (硬编码用于测试)
const PrivateKeyPEM = `-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgsVDj0g/D7uNCm8aC
E4TuaiDT4Pgb1IuuZ69YdGNvcAegCgYIKoZIzj0DAQehRANCAARObgGumaESbPMM
SIRDAVLcWemp0fMlnfDE4EHmqcD58arEJWsr3aWEhc4BHocOUIGjko0cVWGchrFa
/T/KG1tr
-----END PRIVATE KEY-----`
func main() {
log.Println("开始测试 Apple IAP API 连接...")
log.Printf("环境: %v (Sandbox=%v)\n", func() string {
if IsSandbox {
return "沙盒 (Sandbox)"
}
return "生产 (Production)"
}(), IsSandbox)
log.Printf("KeyID: %s\n", KeyID)
log.Printf("IssuerID: %s\n", IssuerID)
log.Printf("BundleID: %s\n", BundleID)
log.Printf("TransactionID: %s\n", TestTransactionID)
token, err := buildAPIToken()
if err != nil {
log.Fatalf("生成 JWT Token 失败: %v", err)
}
log.Println("JWT Token 生成成功")
// 发起请求
host := "https://api.storekit.itunes.apple.com"
if IsSandbox {
host = "https://api.storekit-sandbox.itunes.apple.com"
}
url := fmt.Sprintf("%s/inApps/v1/transactions/%s", host, TestTransactionID)
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+token)
log.Printf("正在请求: %s", url)
start := time.Now()
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
duration := time.Since(start)
body, _ := io.ReadAll(resp.Body)
log.Printf("请求耗时: %v", duration)
log.Printf("状态码: %d", resp.StatusCode)
if resp.StatusCode == 200 {
log.Println("✅ 测试成功!API 调用正常。")
log.Printf("响应内容: %s", string(body))
} else {
log.Println("❌ 测试失败!")
log.Printf("错误响应: %s", string(body))
if resp.StatusCode == 401 {
log.Println("原因分析: 401 Unauthorized 通常表示:")
log.Println("1. Key ID 或 Issuer ID 错误")
log.Println("2. Bundle ID 不匹配")
log.Println("3. 私钥错误")
log.Println("4. Token 格式错误 (如算法或 Claims)")
} else if resp.StatusCode == 404 {
log.Println("原因分析: 404 Not Found 通常表示 Transaction ID 不存在或环境(沙盒/生产)选错了")
}
}
}
// 下面是复制过来的工具函数
func buildAPIToken() (string, error) {
header := map[string]interface{}{
"alg": "ES256",
"kid": KeyID,
"typ": "JWT",
}
now := time.Now().Unix()
payload := map[string]interface{}{
"iss": IssuerID,
"iat": now,
"exp": now + 60, // 测试 Token 有效期短一点即可
"aud": "appstoreconnect-v1",
}
if BundleID != "" {
payload["bid"] = BundleID
}
hb, _ := json.Marshal(header)
pb, _ := json.Marshal(payload)
enc := func(b []byte) string {
return base64.RawURLEncoding.EncodeToString(b)
}
unsigned := fmt.Sprintf("%s.%s", enc(hb), enc(pb))
block, _ := pem.Decode([]byte(PrivateKeyPEM))
if block == nil {
return "", fmt.Errorf("invalid private key")
}
keyAny, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return "", err
}
priv, ok := keyAny.(*ecdsa.PrivateKey)
if !ok {
return "", fmt.Errorf("private key is not ECDSA")
}
digest := sha256Sum([]byte(unsigned))
r, s, err := ecdsa.Sign(rand.Reader, priv, digest)
if err != nil {
return "", err
}
curveBits := priv.Curve.Params().BitSize
keyBytes := curveBits / 8
if curveBits%8 > 0 {
keyBytes += 1
}
rBytes := r.Bytes()
rBytesPadded := make([]byte, keyBytes)
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
sBytes := s.Bytes()
sBytesPadded := make([]byte, keyBytes)
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
sig := append(rBytesPadded, sBytesPadded...)
return unsigned + "." + base64.RawURLEncoding.EncodeToString(sig), nil
}
func sha256Sum(b []byte) []byte {
h := sha256.New()
h.Write(b)
return h.Sum(nil)
}
-111
View File
@@ -1,111 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
pkgaes "github.com/perfect-panel/server/pkg/aes"
)
// 替换为您实际的服务器地址
const BaseURL = "https://api.hifast.biz"
// 替换为您实际的用户登录 Token (Authorization: Bearer <token>)
const UserToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJEZXZpY2VJZCI6MzgzLCJMb2dpblR5cGUiOiJkZXZpY2UiLCJTZXNzaW9uSWQiOiIwMTliMmFmZC1jMjUwLTc1YmItODQzMy04NDMyNWVmZGRkMzMiLCJVc2VySWQiOjM4MywiZXhwIjoxNzY2NTU3NjMyLCJpYXQiOjE3NjU5NTI4MzJ9.kkcT4ojXG9qn_aVqMaGqUUXhHcZXHy49k5Vn05Et9OM"
// 替换为您在后台配置的设备通信密钥 (Security Secret)
const DeviceSecret = "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx"
// 替换为您要测试的 Transaction ID
const TestTransactionID = "2000001083238483"
func main() {
fmt.Println("开始测试 Restore 接口 (AES 加密模式)...")
fmt.Printf("目标 Transaction ID: %s\n", TestTransactionID)
// 1. 构造原始请求数据
payload := map[string]interface{}{
"transactions": []string{TestTransactionID},
}
payloadBytes, _ := json.Marshal(payload)
fmt.Printf("原始请求体: %s\n", string(payloadBytes))
// 2. 加密数据
if DeviceSecret == "YOUR_DEVICE_SECRET_HERE" {
log.Fatal("❌ 请在代码中设置 DeviceSecret (对应后台配置的 Security Secret)")
}
encryptedData, iv, err := pkgaes.Encrypt(payloadBytes, DeviceSecret)
if err != nil {
log.Fatalf("加密失败: %v", err)
}
// 3. 构造最终的请求体 (符合 DeviceMiddleware 要求的格式)
// DeviceMiddleware 期望的格式是: { "data": "Base64Cipher", "time": "Nonce/IV" }
// 或者直接在 URL Query 中传 ?data=...&time=...
// 这里我们模拟 POST JSON body 的方式
finalPayload := map[string]interface{}{
"data": encryptedData,
"time": iv,
}
finalBytes, _ := json.Marshal(finalPayload)
fmt.Printf("加密后请求体: %s\n", string(finalBytes))
url := fmt.Sprintf("%s/v1/public/iap/apple/restore", BaseURL)
req, _ := http.NewRequest("POST", url, strings.NewReader(string(finalBytes)))
req.Header.Set("Content-Type", "application/json")
// 添加必要的 Header 以通过 DeviceMiddleware
req.Header.Set("Login-Type", "device") // 触发 DeviceMiddleware 的解密逻辑
// 注意:这里需要替换为真实有效的 Bearer Token,否则会报 401
// 您可以先登录后台或者使用 cmd/test_apple_iap 工具生成的 token 也是不可用的,必须是业务系统的 token
// 为了演示,这里留空,实际运行前请填入
if UserToken != "YOUR_USER_TOKEN_HERE" {
req.Header.Set("Authorization", "Bearer "+UserToken)
} else {
fmt.Println("⚠️ 警告: 未设置 UserToken,请求可能会失败 (401 Unauthorized)")
}
client := &http.Client{Timeout: 10 * time.Second}
start := time.Now()
resp, err := client.Do(req)
if err != nil {
log.Fatalf("请求失败: %v", err)
}
defer resp.Body.Close()
duration := time.Since(start)
body, _ := io.ReadAll(resp.Body)
fmt.Printf("请求耗时: %v\n", duration)
fmt.Printf("状态码: %d\n", resp.StatusCode)
fmt.Printf("响应内容: %s\n", string(body))
if resp.StatusCode == 200 {
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err == nil {
// 检查业务状态码
if code, ok := result["code"].(float64); ok && int(code) != 200 {
fmt.Printf("❌ 业务处理失败: code=%d, msg=%s\n", int(code), result["msg"])
return
}
fmt.Println("✅ Restore 接口调用成功!")
if data, ok := result["data"].(map[string]interface{}); ok {
if success, ok := data["success"].(bool); ok && success {
fmt.Println(" 业务处理成功: success=true")
} else {
fmt.Println(" 业务处理结果未知:", data)
}
} else {
fmt.Println(" 无数据返回或格式不符")
}
}
} else {
fmt.Println("❌ 接口调用失败")
}
}
+73
View File
@@ -0,0 +1,73 @@
package cmd
import (
"fmt"
"github.com/perfect-panel/server/pkg/updater"
"github.com/spf13/cobra"
)
var (
checkOnly bool
)
var updateCmd = &cobra.Command{
Use: "update",
Short: "Check for updates and update PPanel to the latest version",
Long: `Check for available updates from GitHub releases and automatically
update the PPanel binary to the latest version.
Examples:
# Check for updates only
ppanel-server update --check
# Update to the latest version
ppanel-server update`,
Run: func(cmd *cobra.Command, args []string) {
u := updater.NewUpdater()
if checkOnly {
checkForUpdates(u)
return
}
performUpdate(u)
},
}
func init() {
updateCmd.Flags().BoolVarP(&checkOnly, "check", "c", false, "Check for updates without applying them")
}
func checkForUpdates(u *updater.Updater) {
fmt.Println("Checking for updates...")
release, hasUpdate, err := u.CheckForUpdates()
if err != nil {
fmt.Printf("Error checking for updates: %v\n", err)
return
}
if !hasUpdate {
fmt.Println("You are already running the latest version!")
return
}
fmt.Printf("\nNew version available!\n")
fmt.Printf("Current version: %s\n", u.CurrentVersion)
fmt.Printf("Latest version: %s\n", release.TagName)
fmt.Printf("\nRelease notes:\n%s\n", release.Body)
fmt.Printf("\nTo update, run: ppanel-server update\n")
}
func performUpdate(u *updater.Updater) {
fmt.Println("Starting update process...")
if err := u.Update(); err != nil {
fmt.Printf("Update failed: %v\n", err)
return
}
fmt.Println("\nUpdate completed successfully!")
fmt.Println("Please restart the application to use the new version.")
}
-101
View File
@@ -1,101 +0,0 @@
# 客户端错误上报接口文档
## 接口概览
- 路径:`POST /v1/common/log/message/report`
- 说明:APP/PC/Web 客户端错误与异常信息上报,服务端入库 `log_message` 表,并提供管理端查询。
- 认证:无需登录;若走设备安全通道需使用 `Login-Type: device` 与 AES 加密。
- 中间件:`DeviceMiddleware`(当请求头 `Login-Type=device` 时启用加解密)。
## 请求
- Headers
- `Content-Type: application/json`
- 可选:`Login-Type: device`(启用设备加密通道)
- BodyJSON):
- `platform` string 必填,客户端平台,如 `android`/`ios`/`windows`/`mac`/`web`
- `appVersion` string 可选,应用版本,如 `2.4.1`
- `osName` string 可选,系统名称,如 `Android`/`iOS`/`Windows`/`macOS`
- `osVersion` string 可选,系统版本,如 `14`
- `deviceId` string 可选,设备唯一标识
- `sessionId` string 可选,会话标识
- `level` uint8 可选,日志等级:`1=fatal``2=error``3=warn``4=info`(默认 `3`
- `errorCode` string 可选,业务或系统错误码
- `message` string 必填,错误简述(服务器将超过约 64KB 的内容截断)
- `stack` string 可选,堆栈信息(服务器将超过约 1MB 的内容截断)
- `context` object 可选,扩展上下文(如接口路径、参数、网络状态等)
- `occurredAt` int64 可选,客户端发生时间,毫秒时间戳
- 服务器侧自动填充:
- `client_ip``user_agent``locale` 由请求解析
- `user_id` 仅在鉴权后由服务端注入(本接口默认匿名)
## 加密通道(设备)
- 当使用设备安全通道时(`Login-Type: device`),请求体需要将原始 JSON 加密为:
- `data` stringAES 加密后的密文
- `time` stringIV/nonce(与密文配套)
- 服务端会自动解密为明文 JSON,再进行字段校验与入库。
## 响应
- 成功:
```
{
"code": 0,
"msg": "OK",
"data": { "id": 123 }
}
```
- 常见错误:
- `{"code":401,"msg":"TooManyRequests"}` 触发速率限制(设备ID或IP维度)
- `{"code":400,"msg":"InvalidParams"}` 参数校验失败(缺少必填或类型不符)
- `{"code":10001,"msg":"DatabaseQueryError"}` 数据库操作异常
## 速率限制
- 默认按 `deviceId``client_ip` 每分钟最多约 `120` 条,超限即返回 `TooManyRequests`
## 去重策略
- 服务端计算 `digest = sha256(message|stack|errorCode|appVersion|platform)` 并尝试唯一入库,重复日志可能返回已存在记录的 `id`
## 示例
- 明文上报(推荐测试使用):
```
curl -X POST http://localhost:8080/v1/common/log/message/report \
-H 'Content-Type: application/json' \
-d '{
"platform": "android",
"appVersion": "2.4.1",
"osName": "Android",
"osVersion": "14",
"deviceId": "and-9a7f3e2c-01",
"sessionId": "sess-73f8a2a4",
"level": 2,
"errorCode": "ORDER_RENEWAL_TIMEOUT",
"message": "订单续费接口超时:/v1/public/order/renewal",
"stack": "TimeoutException: request exceeded 8000ms\\n at HttpClient.post(HttpClient.kt:214)\\n at RenewalRepository.submit(RenewalRepository.kt:87)",
"context": {
"api": "/v1/public/order/renewal",
"method": "POST",
"endpoint": "https://api.example.com/v1/public/order/renewal",
"httpStatus": 504,
"responseTimeMs": 8123,
"retryCount": 2,
"network": { "type": "cellular", "carrier": "China Mobile" }
},
"occurredAt": 1733200005123
}'
```
- 设备加密上报(示意):
```
# 将原始JSON通过AES加密得到密文data与随机IV time
curl -X POST http://localhost:8080/v1/common/log/message/report \
-H 'Content-Type: application/json' \
-H 'Login-Type: device' \
-d '{"data":"<aes_cipher>","time":"<iv_nonce>"}'
```
## 管理端查询(用于联调验证)
- 列表:`GET /v1/admin/log/message/error/list`(需 `Authorization`
- 详情:`GET /v1/admin/log/message/error/detail?id=...`
## 备注
- 字段尽量避免携带敏感信息(密码、密钥、完整令牌等);如需调试请截断或脱敏后上报。
- 建议在 APP/PC 端统一封装上报模块,包含:字段收集、级别过滤、采样策略、离线缓存与重试、速率限制配合。
-40
View File
@@ -159,43 +159,3 @@ Administer: # 管理员登录配置
- **JWT**:为 `JwtAuth``AccessSecret` 设置强密钥以增强安全性。
如需进一步帮助,请参考 PPanel 官方文档或联系支持团队。
## 6. Apple IAP(非续期订阅)配置
- 通过 `Site.CustomData` 配置内购商品与权益映射,示例:
```json
{
"iapProductMap": {
"com.airport.vpn.pass.30d": {
"description": "30天通行证",
"priceText": "¥28.00",
"durationDays": 30,
"tier": "Basic",
"subscribeId": 1001
},
"com.airport.vpn.pass.90d": {
"description": "90天通行证",
"priceText": "¥68.00",
"durationDays": 90,
"tier": "Pro",
"subscribeId": 1002
}
},
"iapBundleId": "co.airoport.app.ios"
}
```
- 字段说明:
- `iapProductMap``productId → 映射`,用于后端计算到期时间与绑定内部计划(`subscribeId`)。
- `description`/`priceText`:客户端展示文案。
- `durationDays`:非续期订阅的有效天数。
- `tier`:权益等级标签,用于状态返回。
- `subscribeId`:绑定到现有 `subscribe` 计划 ID。
- `iapBundleId`:客户端 Bundle ID(可用于后端基础校验)。
### 接口速览
- `GET /v1/public/iap/apple/products`:返回可售商品与文案(基于 `iapProductMap`)。
- `POST /v1/public/iap/apple/transactions/attach`:绑定一次购买到用户,入参 `signed_transaction_jws`
- `POST /v1/public/iap/apple/restore`:恢复历史购买(批量 JWS)。
- `GET /v1/public/iap/apple/status`:返回用户的 IAP 权益状态与到期时间。
-228
View File
@@ -1,228 +0,0 @@
# iOS 内购接入与接口调用指南(StoreKit 2 + 服务端接口)
## 概述
本指南面向 iOS App 开发者,说明使用 StoreKit 2 完成「非续期订阅/非消耗型」的购买、验证与绑定流程,并与后端接口打通,实现用户权益发放与恢复。
## 商品与映射
- Apple 端必须在 App Store Connect 创建对应 `productId` 的内购商品(非续期订阅或非消耗型)。
- 服务端维护「商品映射」:`productId → {durationDays, tier, subscribeId}`,用于计算到期与绑定内部订阅计划(`subscribeId`)。
- 若某 `productId` 暂未在服务端配置,客户端可在绑定请求中携带回退字段:`duration_days``subscribe_id``tier`。服务端将按 App 的定义进行绑定。
## 客户端整体流程(StoreKit 2
1) 检查支付能力
- `if !AppStore.canMakePayments { 隐藏商店并提示 }`
2) 拉取商品
- 通过已知 `productId` 列表调用 `Product.products(for:)`,展示价格与描述
3) 发起购买并本地验证
- 调用 `try await product.purchase()` 弹出系统确认表单
- 成功后 `let transaction = try verification.payloadValue`,并取到 `transaction.signedData`JWS
4) 绑定购买(服务端 attach)
-`signedData` 作为 `signed_transaction_jws` POST 至 `/v1/public/iap/apple/transactions/attach`
- 若服务端未配置该 `productId`,同时携带:`duration_days`(有效天数)、`subscribe_id`(内部订阅计划 ID)、`tier`(展示用标签)
5) 恢复购买(restore
- `try await AppStore.sync()` 后,遍历 `Transaction.currentEntitlements` 并逐条 `verify()`
- 收集每条 `signedData`,批量 POST 至 `/v1/public/iap/apple/restore`
6) 查询状态(status
- `GET /v1/public/iap/apple/status` 获取 `active/expires_at/tier`,用于 UI 展示与权限控制
7) 退款入口(HIG 建议)
- 在购买帮助页提供「请求退款」按钮,调用 `beginRefundRequest(for:in:)`
## 接口详细
所有接口均需要携带用户登录态的 `Authorization: Bearer <token>`
- 绑定购买(attach
- `POST /v1/public/iap/apple/transactions/attach`
- 请求体(映射一致时,仅需 `signed_transaction_jws`):
```json
{
"signed_transaction_jws": "<StoreKit返回的signedData>"
}
```
- 请求体(映射不一致时的回退):
```json
{
"signed_transaction_jws": "<signedData>",
"duration_days": 30,
"subscribe_id": 1001,
"tier": "Basic"
}
```
- 响应示例:
```json
{
"code": 200,
"msg": "success",
"data": { "expires_at": 1736860000, "tier": "Basic" }
}
```
- 恢复购买(restore
- `POST /v1/public/iap/apple/restore`
- 请求体:
```json
{
"transactions": ["<signedData-1>", "<signedData-2>"]
}
```
- 响应示例:
```json
{
"code": 200,
"msg": "success",
"data": { "success": true }
}
```
- 查询状态(status
- `GET /v1/public/iap/apple/status`
- 响应示例:
```json
{
"code": 200,
"msg": "success",
"data": { "active": true, "expires_at": 1736860000, "tier": "Basic" }
}
```
## 收银台统一返回契约(含 Apple IAP)
- 统一下单接口返回的 `type` 用于客户端决定支付方式;当为 Apple IAP 时,返回 Apple 商品 ID 列表,前端直接用 StoreKit 购买:
- Apple IAP 收银台返回示例(建议规范):
```json
{
"code": 200,
"msg": "success",
"data": {
"type": "apple_iap",
"product_ids": [
"merchant.hifastvpn.day7",
"merchant.hifastvpn.day30"
],
"hint": "Use StoreKit to purchase the given product_ids, then POST signedData to attach."
}
}
```
- 其他支付平台保持原有结构:
- Stripe`{ "type":"stripe", "stripe": { "publishable_key": "...", "client_secret":"..." } }`
- URL 跳转:`{ "type":"url", "checkout_url": "https://..." }`
- 二维码:`{ "type":"qr", "checkout_url": "..." }`
### 前端处理逻辑(统一出口)
- 收银台响应分流:
- `type === "apple_iap"` → 取 `product_ids`,用 StoreKit 拉取并购买,成功后将 `signedData` 调用 `attach`
- `type === "stripe"` → 初始化 Stripe 支付组件
- `type === "url"` → 直接跳转到返回的 `checkout_url`
- `type === "qr"` → 展示二维码URL
### 命名规则(推荐)
- Apple 商品命名统一为:`merchant.hifastvpn.day${quantity}`,例如:
- 7 天:`merchant.hifastvpn.day7`
- 30 天:`merchant.hifastvpn.day30`
- 90 天:`merchant.hifastvpn.day90`
- 新增套餐时只需:在 App Store Connect 新增对应商品,并在 Web 后台/配置新增映射(`durationDays/tier/subscribeId`),前端无需改代码即可使用。
## Swift 示例
### 拉取商品与展示
```swift
import StoreKit
let productIds = ["com.airport.vpn.pass.30d", "com.airport.vpn.pass.90d"]
let products = try await Product.products(for: productIds)
// 展示 products 的价格与描述
```
### 发起购买并绑定
```swift
import StoreKit
func purchaseAndAttach(product: Product, token: String) async throws {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try verification.payloadValue
let jws = transaction.signedData
struct AttachReq: Codable {
let signed_transaction_jws: String
// 若映射不一致,则补齐以下字段
let duration_days: Int64?
let subscribe_id: Int64?
let tier: String?
}
let body = AttachReq(
signed_transaction_jws: jws,
duration_days: nil, // 映射一致时可为 nil
subscribe_id: nil,
tier: nil
)
var req = URLRequest(url: URL(string: "https://api.yourdomain.com/v1/public/iap/apple/transactions/attach")!)
req.httpMethod = "POST"
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONEncoder().encode(body)
let (data, _) = try await URLSession.shared.data(for: req)
// 解析返回并更新 UI
default:
break
}
}
```
### 恢复购买(批量)
```swift
import StoreKit
func restorePurchases(token: String) async throws {
try await AppStore.sync()
var signedDataList: [String] = []
for await t in Transaction.currentEntitlements {
let v = try t.verificationResult.payloadValue
signedDataList.append(v.signedData)
}
struct RestoreReq: Codable { let transactions: [String] }
let body = RestoreReq(transactions: signedDataList)
var req = URLRequest(url: URL(string: "https://api.yourdomain.com/v1/public/iap/apple/restore")!)
req.httpMethod = "POST"
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONEncoder().encode(body)
let (_, _) = try await URLSession.shared.data(for: req)
}
```
### 查询状态
```swift
func fetchIAPStatus(token: String) async throws {
var req = URLRequest(url: URL(string: "https://api.yourdomain.com/v1/public/iap/apple/status")!)
req.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: req)
// 解析 active/expires_at/tier
}
```
### 退款入口(帮助页)
```swift
// 在 App 内购买帮助页面提供按钮,调用系统退款流程
// try await beginRefundRequest(for: product, in: windowScene)
```
## 错误处理建议
- 绑定失败(`code != 200`
- 校验用户登录态、`signedData` 是否来自 `transaction.verify()` 的成功结果
- 若提示 `unknown product`,请在请求体中按回退规范携带 `duration_days/subscribe_id/tier`
- 网络与重试
- 建议对 `attach/restore` 做有限重试与幂等保护(相同 `originalTransactionId` 不重复绑定)
- 恢复失败
- 确保已调用 `AppStore.sync()` 并遍历 `currentEntitlements`
## 调试与沙盒
- 使用 Sandbox 账号进行测试;购买成功后调用 `attach → status`,验证到期与等级;再次 `restore → status` 验证幂等。
- 建议打印 `request_id`(如有)便于后端排查。
## HIG 注意事项
- 仅在可支付时显示商店;价格与文案清晰,不截断标题;使用系统确认表单,不自定义购买弹窗。
- 在帮助页提供退款入口与说明,文案简洁直达。
## 常见问题
- `productId` 不一致:服务端未配置某商品时,客户端按回退规范补齐时长与订阅 ID 即可绑定;建议后续让两端保持一致以减少维护成本。
- 权益冲突:若用户同时存在多来源订阅,服务端按最高等级与最晚到期计算权益。
-113
View File
@@ -1,113 +0,0 @@
# 项目加解密使用说明
本指南介绍了 PPanel Server 项目中使用的加解密机制,主要用于设备端(Device)通信的安全保障。
## 1. 核心算法
项目使用 **AES-256-CBC** 加密算法。
- **填充方式**PKCS7 Padding。
- **数据编码**Base64。
## 2. 密钥(Key)与初始化向量(IV)生成逻辑
### 2.1 密钥生成 (Key Generation)
密钥由一个预定义的 `SecuritySecret`(简称 Secret)生成:
1. 对 Secret 进行 **SHA-256** 哈希。
2. 取哈希结果的前 **32 字节** 作为 AES-256 的密钥。
### 2.2 初始化向量生成 (IV Generation)
IV 是动态生成的,以增强安全性:
1. 客户端或服务端生成一个随机字符串(Nonce,通常是纳秒级时间戳)。
2. 对 Nonce 进行 **MD5** 哈希。
3. 将 MD5 结果(十六进制字符串)与 Secret 拼接。
4. 对拼接后的字符串按 2.1 节的方式生成密钥逻辑处理,取结果的前 **16 字节** 作为 IV。
> [!NOTE]
> 在 API 通信中,Nonce 字符串通常通过请求参数中的 `time` 字段传递。
## 3. 身份识别与优先顺序
服务端通过 `Login-Type` 来识别是否需要进行加解密逻辑(值为 `device` 时触发)。
### 3.1 识别途径
1. **Token 负载 (JWT Claims)**Token 中包含 `LoginType` (值为 `device`) 和 `DeviceId`
2. **请求头 (Header)**`Login-Type: device`
### 3.2 优先顺序与场景
- **已登录场景**:服务端优先从 **Token** 负载中读取 `LoginType`。如果 Token 合法且包含 `LoginType: device`,则启用加解密。
- **未登录/登录中场景**:例如 `/v1/auth/login/device` 接口,由于此时没有有效 Token,服务端会检查 **Header** 中的 `Login-Type`
> [!TIP]
> 为了确保一致性,建议在设备端请求中**始终**携带 `Login-Type: device` 请求头,并在登录后确保存储的 Token 负载中也包含对应信息。
## 4. 中间件应用 (DeviceMiddleware)
`DeviceMiddleware` 处理 `Login-Type: device` 的请求:
- **请求解密**
- 检查 URL 参数或 JSON Body 中的 `data`(加密数据)和 `time`Nonce)。
- 使用配置的 Secret 和 Nonce 解密 `data`
- 将解密后的 JSON 重新注入到请求上下文中。
- **响应加密**
- 拦截响应 Body。
- 加密 Body 中的 `data` 字段。
- 将响应格式化为:
```json
{
"data": "ENCRYPTED_BASE64_STRING",
"time": "NONCE_STRING"
}
```
## 5. Token 负载详情 (JWT Payload)
当 `Login-Type` 为 `device` 时,JWT Token 会包含以下自定义字段:
- `LoginType`: `"device"`
- `DeviceId`: 设备的数据库唯一 ID。
## 6. 代码示例
### Go 语言 (服务端)
参考 [pkg/aes/aes.go](file:///Users/Apple/vpn/ppanel-server/pkg/aes/aes.go)
```go
import pkgaes "github.com/perfect-panel/server/pkg/aes"
// 加密
encrypt, nonce, err := pkgaes.Encrypt([]byte("plain text"), secret)
// 解密
decrypt, err := pkgaes.Decrypt(cipherText, secret, nonce)
```
### JavaScript (客户端示例)
使用 `crypto-js` 库:
```javascript
const CryptoJS = require("crypto-js");
function getIv(nonce, secret) {
const md5Nonce = CryptoJS.MD5(nonce).toString();
const ivStr = md5Nonce + secret;
const key = CryptoJS.SHA256(ivStr);
return CryptoJS.enc.Hex.parse(key.toString().substring(0, 32));
}
function getKey(secret) {
const key = CryptoJS.SHA256(secret);
return CryptoJS.enc.Hex.parse(key.toString().substring(0, 64));
}
// 加密示例
const key = getKey(secret);
const iv = getIv(nonce, secret);
const encrypted = CryptoJS.AES.encrypt("plain text", key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
console.log(encrypted.toString()); // Base64
```
## 5. 安全建议
- 请务必在配置文件中修改默认的 `SecuritySecret`。
- 确保 `time` (Nonce) 在每次请求时都是唯一的,以防止重放攻击和频率分析。
-42
View File
@@ -1,42 +0,0 @@
# 报错日志收集(log_message
## 项目规划
- 目标:新增 `log_message` 表与采集/查询接口,用于 APP / PC / Web 客户端错误日志收集与分析。
- 范围:
- 创建 MySQL 表与迁移脚本(02105)。
- 新增客户端上报接口 `POST /v1/common/log/message/report`
- 新增管理端查询接口 `GET /v1/admin/log/message/error/list``GET /v1/admin/log/message/error/detail`
## 实施方案
- 表结构:见 `initialize/migrate/database/02120_log_message.up.sql`
- 模型:`internal/model/logmessage/`(实体、默认 CRUD、筛选)。
- 路由:在 `internal/handler/routes.go` 注册公共与管理端路由。
- 逻辑:
- 上报逻辑:`internal/logic/common/logMessageReportLogic.go`(限流、指纹去重、入库)。
- 管理查询:`internal/logic/admin/log/getErrorLogMessageListLogic.go``getErrorLogMessageDetailLogic.go`
- 类型:`internal/types/types.go` 新增请求/响应结构。
- 安全:详细加解密逻辑见 [加解密说明文档.md](file:///Users/Apple/vpn/ppanel-server/doc/加解密说明文档.md)。
## 进度记录
- 2025-12-02
- 完成表与索引创建迁移文件。
- 完成模型与服务注入。
- 完成公共上报接口与限流、去重逻辑;编译验证通过。
- 完成管理端列表与详情接口;编译验证通过。
- 待办:根据运营需求调整限流阈值与日志保留策略。
- 2026-01-08
- 完成「项目加解密使用说明」文档编写,涵盖 AES-256-CBC 实现及中间件逻辑。
## 接口规范
- 上报:`POST /v1/common/log/message/report`(详见 `doc/api/log_message_report.md`
- 管理端列表:`GET /v1/admin/log/message/error/list`
- 筛选:platform、level、user_id、device_id、error_code、keyword、start、end;分页:page、size
- 响应:`{ total, list }`
- 管理端详情:`GET /v1/admin/log/message/error/detail?id=...`
- 响应:完整字段
## 保留策略与安全
- 限流:按设备/IP 每分钟 120 条(可配置)。
- 隐私:避免采集敏感数据;服务端对大字段做长度限制与截断。
-23
View File
@@ -1,23 +0,0 @@
version: '3'
services:
jaeger:
image: jaegertracing/all-in-one:latest
container_name: jaeger
ports:
- "16686:16686"
- "4317:4317"
- "4318:4318"
environment:
# - SPAN_STORAGE_TYPE=elasticsearch
# - ES_SERVER_URLS=http://elasticsearch:9200
- LOG_LEVEL=debug
- COLLECTOR_OTLP_ENABLED=true
deploy:
resources:
limits:
cpus: '0.8'
memory: 500M
reservations:
cpus: '0.05'
memory: 200M
@@ -1,41 +0,0 @@
# 设备移出和邀请码优化 - 验收报告
## 修复内容回顾
### 1. 设备移出后未自动退出
- **修复点 1**:在 `bindEmailWithVerificationLogic.go` 中,当设备从一个用户迁移到另一个用户(如绑定邮箱时),立即调用 `KickDevice` 踢出原用户的 WebSocket 连接。
- **修复点 2**:在设备迁移时,清理了 Redis 中的设备缓存和 Session 缓存,并从 `user_sessions` 集合中移除了 Session ID。
- **修复点 3**:在 `unbindDeviceLogic.go` 中,解绑设备时补充了 `user_sessions` 集合的清理逻辑,确保 Session 被完全移除。
### 2. 邀请码错误提示不友好
- **修复点**:在 `bindInviteCodeLogic.go` 中,捕获 `gorm.ErrRecordNotFound` 错误,并返回错误码 `20009` (InviteCodeError) 和提示 "无邀请码"。
---
## 验证结果
### 自动化验证
- [x] 代码编译通过 (`go build ./...`)
- [x] 静态检查通过
### 场景验证(逻辑推演)
**场景 1:设备B绑定邮箱后被移除**
1. 设备B绑定邮箱,执行迁移逻辑。
2. `KickDevice(originalUserId, deviceIdentifier)` 被调用 -> 设备B的 WebSocket 连接断开。
3. Redis 中 `device:identifier``session:id` 被删除 -> Token 失效。
4. 用户在设备A上操作移除设备B -> `unbindDeviceLogic` 执行 -> 再次尝试踢出和清理(防御性)。
5. **结果**:设备B立即离线且无法继续使用。
**场景 2:输入错误邀请码**
1. 调用绑定接口, `FindOneByReferCode` 返回 `RecordNotFound`
2. 逻辑捕获错误,返回 `InviteCodeError`
3. **结果**:前端收到 20009 错误码和 "无邀请码" 提示。
---
## 遗留问题 / 注意事项
-
## 结论
修复已完成,符合预期。
@@ -1,160 +0,0 @@
# 设备管理系统 Bug 分析 - 最终确认版
## 场景还原
### 用户操作流程
1. **设备A** 最初通过设备登录(DeviceLogin),系统自动创建用户1 + 设备A记录
2. **设备B** 最初也通过设备登录,系统自动创建用户2 + 设备B记录
3. **设备A** 绑定邮箱 xxx@example.com,用户1变为"邮箱+设备"用户
4. **设备B** 绑定**同一个邮箱** xxx@example.com
- 系统发现邮箱已存在,执行设备转移
- 设备B 从用户2迁移到用户1
- 用户2被删除
- 现在用户1拥有:设备A + 设备B + 邮箱认证
5. **在设备A上操作**,从设备列表移除设备B
6. **问题**:设备B没有被踢下线,仍然能使用
---
## 数据流分析
### 绑定邮箱后的状态(第4步后)
```
User 表:
┌─────┬───────────────┐
│ Id │ 用户1 │
└─────┴───────────────┘
user_device 表:
┌─────────────┬───────────┐
│ Identifier │ UserId │
├─────────────┼───────────┤
│ device-a │ 用户1 │
│ device-b │ 用户1 │ <- 设备B迁移到用户1
└─────────────┴───────────┘
user_auth_methods 表:
┌────────────┬────────────────┬───────────┐
│ AuthType │ AuthIdentifier │ UserId │
├────────────┼────────────────┼───────────┤
│ device │ device-a │ 用户1 │
│ device │ device-b │ 用户1 │
│ email │ xxx@email.com │ 用户1 │
└────────────┴────────────────┴───────────┘
DeviceManager (内存 WebSocket 连接):
┌───────────────────────────────────────────────────┐
│ userDevices sync.Map │
├───────────────────────────────────────────────────┤
│ 用户1 -> [Device{DeviceID="device-a", ...}] │
│ 用户2 -> [Device{DeviceID="device-b", ...}] ❌ │ <- 问题!设备B的连接仍在用户2名下
└───────────────────────────────────────────────────┘
```
### 问题根源
**设备B绑定邮箱时**`bindEmailWithVerificationLogic.go`):
- ✅ 数据库:设备B的 `UserId` 被更新为用户1
- ❌ 内存:`DeviceManager` 中设备B的 WebSocket 连接仍然在**用户2**名下
- ❌ 缓存:`device:device-b` -> 旧的 sessionId(可能关联用户2
**解绑设备B时**`unbindDeviceLogic.go`):
```go
// 第 48 行:验证设备属于当前用户
if device.UserId != u.Id { // device.UserId=用户1, u.Id=用户1, 验证通过
return errors.Wrapf(...)
}
// 第 123 行:踢出设备
l.svcCtx.DeviceManager.KickDevice(u.Id, identifier)
// KickDevice(用户1, "device-b")
```
**KickDevice 执行时**
```go
func (dm *DeviceManager) KickDevice(userID int64, deviceID string) {
val, ok := dm.userDevices.Load(userID) // 查找用户1的设备列表
// 用户1的设备列表只有 device-a
// 找不到 device-b!因为 device-b 的连接还在用户2名下
}
```
---
## 根本原因总结
| 操作 | 数据库 | DeviceManager 内存 | Redis 缓存 |
|------|--------|-------------------|------------|
| 设备B绑定邮箱 | ✅ 更新 UserId | ❌ 未更新 | ❌ 未清理 |
| 解绑设备B | ✅ 创建新用户 | ❌ 找不到设备 | ✅ 尝试清理 |
**核心问题**:设备绑定邮箱(转移用户)时,没有更新 `DeviceManager` 中的连接归属。
---
## 修复方案
### 方案1:在绑定邮箱时踢出旧连接(推荐)
`bindEmailWithVerificationLogic.go` 迁移设备后,踢出设备的旧连接:
```go
// 迁移设备到邮箱用户后
for _, device := range devices {
// 更新设备归属
device.UserId = emailUserId
err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device)
// 新增:踢出旧连接(使用原用户ID)
l.svcCtx.DeviceManager.KickDevice(u.Id, device.Identifier)
}
```
### 方案2:在解绑时遍历所有用户查找设备
修改 `KickDevice``unbindDeviceLogic` 逻辑,不依赖用户ID查找设备。
### 方案3:清理 Redis 缓存使旧 Token 失效
确保设备转移后,旧的 session 和 device 缓存被清理:
```go
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, device.Identifier)
if sessionId, _ := l.svcCtx.Redis.Get(ctx, deviceCacheKey).Result(); sessionId != "" {
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
l.svcCtx.Redis.Del(ctx, deviceCacheKey, sessionIdCacheKey)
}
```
---
## 推荐修复策略
**双管齐下**
1. **修复 `bindEmailWithVerificationLogic.go`**
- 设备转移后立即踢出旧连接
- 清理旧用户的缓存
2. **修复 `unbindDeviceLogic.go`**(防御性编程):
- 补充 `user_sessions` 清理逻辑(参考 `deleteUserDeviceLogic.go`
---
## 涉及文件
| 文件 | 修改内容 |
|------|----------|
| `internal/logic/public/user/bindEmailWithVerificationLogic.go` | 设备转移后踢出旧连接 |
| `internal/logic/public/user/unbindDeviceLogic.go` | 补充 user_sessions 清理 |
---
## 验收标准
1. 设备B绑定邮箱后,设备B的旧连接被踢出
2. 从设备A解绑设备B后,设备B立即被踢下线
3. 设备B的 Token 失效,无法继续调用 API
@@ -1,117 +0,0 @@
# 设备移出和邀请码优化 - 共识文档(更新版)
## 需求概述
修复两个 Bug
1. **Bug 1**:设备B绑定邮箱后被从设备A移除,设备B没有被踢下线
2. **Bug 2**:输入不存在的邀请码时,提示信息不友好
---
## Bug 1:设备移出后未自动退出
### 根本原因
设备B绑定邮箱(迁移到邮箱用户)时:
- ✅ 数据库更新了设备的 `UserId`
-`DeviceManager` 内存中设备B的 WebSocket 连接仍在**原用户**名下
- ❌ Redis 缓存中设备B的 session 未被清理
解绑设备B时,`KickDevice(用户1, "device-b")` 在用户1的设备列表中找不到 device-b(因为连接还在原用户名下)。
### 修复方案
**文件1`bindEmailWithVerificationLogic.go`**
在设备迁移后,踢出旧连接并清理缓存:
```go
// 第 139-158 行之后添加
for _, device := range devices {
device.UserId = emailUserId
err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device)
// ...existing code...
// 新增:踢出旧连接并清理缓存
l.svcCtx.DeviceManager.KickDevice(u.Id, device.Identifier)
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, device.Identifier)
if sessionId, _ := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); sessionId != "" {
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
_ = l.svcCtx.Redis.Del(l.ctx, deviceCacheKey).Err()
_ = l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey).Err()
}
}
```
**文件2`unbindDeviceLogic.go`**(防御性修复)
补充 `user_sessions` 清理逻辑,与 `deleteUserDeviceLogic.go` 保持一致:
```go
// 第 118-122 行,补充 sessionsKey 清理
if sessionId, rerr := l.svcCtx.Redis.Get(ctx, deviceCacheKey).Result(); rerr == nil && sessionId != "" {
_ = l.svcCtx.Redis.Del(ctx, deviceCacheKey).Err()
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
_ = l.svcCtx.Redis.Del(ctx, sessionIdCacheKey).Err()
// 新增:清理 user_sessions
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, device.UserId)
_ = l.svcCtx.Redis.ZRem(ctx, sessionsKey, sessionId).Err()
}
```
---
## Bug 2:邀请码错误提示不友好
### 根本原因
`bindInviteCodeLogic.go` 中未区分"邀请码不存在"和"数据库错误"。
### 修复方案
```go
// 第 44-47 行修改为
referrer, err := l.svcCtx.UserModel.FindOneByReferCode(l.ctx, req.InviteCode)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InviteCodeError, "无邀请码"), "invite code not found")
}
logger.WithContext(l.ctx).Error(err)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query referrer failed: %v", err.Error())
}
```
---
## 涉及文件汇总
| 文件 | 修改类型 | 优先级 |
|------|----------|--------|
| `internal/logic/public/user/bindEmailWithVerificationLogic.go` | 核心修复 | 高 |
| `internal/logic/public/user/unbindDeviceLogic.go` | 防御性修复 | 中 |
| `internal/logic/public/user/bindInviteCodeLogic.go` | Bug 修复 | 中 |
---
## 验收标准
### Bug 1 验收
- [ ] 设备B绑定邮箱后,设备B的旧 Token 失效
- [ ] 设备B绑定邮箱后,设备B的 WebSocket 连接被断开
- [ ] 在设备A上移除设备B后,设备B立即被踢下线
- [ ] 设备B无法继续使用旧 Token 调用 API
### Bug 2 验收
- [ ] 输入不存在的邀请码时,返回错误码 20009
- [ ] 错误消息显示"无邀请码"
---
## 验证计划
1. **编译验证**`go build ./...`
2. **手动测试**
- 设备B绑定邮箱 → 检查是否被踢下线
- 设备A移除设备B → 检查设备B是否被踢下线
- 输入无效邀请码 → 检查错误提示
@@ -1,96 +0,0 @@
# 设备移出和邀请码优化 - 设计文档
## 整体架构
本次修复涉及两个独立的 bug,不需要修改架构,只需要修改具体的业务逻辑层代码。
### 组件关系图
```mermaid
graph TB
subgraph "用户请求"
A[客户端] --> B[HTTP Handler]
end
subgraph "业务逻辑层"
B --> C[unbindDeviceLogic]
B --> D[bindInviteCodeLogic]
end
subgraph "服务层"
C --> E[DeviceManager.KickDevice]
D --> F[UserModel.FindOneByReferCode]
end
subgraph "数据层"
E --> G[WebSocket连接管理]
F --> H[GORM/数据库]
end
```
---
## 模块详细设计
### 模块1: UnbindDeviceLogic 修复
#### 当前数据流
```
1. 用户请求解绑设备
2. 验证设备属于当前用户 (device.UserId == u.Id) ✅
3. 事务中:创建新用户,迁移设备
4. 调用 KickDevice(u.Id, identifier) ❌ <-- 用户ID错误
```
#### 修复后数据流
```
1. 用户请求解绑设备
2. 验证设备属于当前用户 ✅
3. 保存原始用户ID: originalUserId := device.UserId ✅
4. 事务中:创建新用户,迁移设备
5. 调用 KickDevice(originalUserId, identifier) ✅ <-- 使用正确的用户ID
```
#### 接口契约
无变化,仅修改内部实现。
---
### 模块2: BindInviteCodeLogic 修复
#### 当前错误处理
```go
if err != nil {
return xerr.DatabaseQueryError // 所有错误统一处理
}
```
#### 修复后错误处理
```go
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return xerr.InviteCodeError("无邀请码") // 记录不存在 → 友好提示
}
return xerr.DatabaseQueryError // 其他错误保持原样
}
```
#### 接口契约
API 返回格式不变,但错误码从 `10001` 变为 `20009`(针对邀请码不存在的情况)。
---
## 异常处理策略
| 场景 | 错误码 | 错误消息 |
|------|--------|----------|
| 邀请码不存在 | 20009 | 无邀请码 |
| 数据库查询错误 | 10001 | Database query error |
| 绑定自己的邀请码 | 20009 | 不允许绑定自己 |
---
## 设计原则
1. **最小改动原则**:只修改必要的代码,不重构现有逻辑
2. **向后兼容**:不改变 API 接口定义
3. **代码风格一致**:遵循项目现有的错误处理模式
@@ -1,20 +0,0 @@
# 设备移出和邀请码优化 - 项目总结
## 项目概览
本次任务修复了两个影响用户体验的 Bug:
1. 设备绑定邮箱后,从设备列表移除时未自动退出。
2. 绑定无效邀请码时,错误提示不友好。
## 关键变更
1. **核心修复**:在设备归属转移(绑定邮箱)时,主动踢出原用户的 WebSocket 连接,防止“幽灵连接”存在。
2. **安全增强**:在设备解绑和转移时,彻底清理 Redis 中的 Session 缓存(包括 `user_sessions` 集合)。
3. **体验优化**:优化了邀请码验证的错误提示,明确告知用户“无邀请码”。
## 文件变更列表
- `internal/logic/public/user/bindEmailWithVerificationLogic.go`
- `internal/logic/public/user/unbindDeviceLogic.go`
- `internal/logic/public/user/bindInviteCodeLogic.go`
## 后续建议
- 建议在测试环境中重点测试多端登录和设备绑定的边界情况。
- 关注 `DeviceManager` 的内存使用情况,确保大量的踢出操作不会造成锁竞争。
@@ -1,91 +0,0 @@
# 设备移出和邀请码优化 - 任务清单
## 任务依赖图
```mermaid
graph LR
A[任务1: 修复设备踢出Bug] --> C[任务3: 编译验证]
B[任务2: 修复邀请码提示Bug] --> C
C --> D[任务4: 更新文档]
```
---
## 原子任务列表
### 任务1: 修复设备解绑后未踢出的问题
**输入契约**
- 文件:`internal/logic/public/user/unbindDeviceLogic.go`
- 当前代码行:第 123 行
**输出契约**
- 在事务执行前保存 `device.UserId`
- 修改 `KickDevice` 调用,使用保存的原始用户ID
**实现约束**
- 不修改方法签名
- 不影响事务逻辑
**验收标准**
- [x] 代码编译通过
- [ ] 解绑设备后,被解绑设备收到踢出消息
**预估复杂度**:低
---
### 任务2: 修复邀请码错误提示不友好的问题
**输入契约**
- 文件:`internal/logic/public/user/bindInviteCodeLogic.go`
- 当前代码行:第 44-47 行
**输出契约**
- 添加 `gorm.ErrRecordNotFound` 判断
- 返回友好的错误消息 "无邀请码"
- 使用 `xerr.InviteCodeError` 错误码
**实现约束**
- 保持与其他模块(如 `userRegisterLogic`)的错误处理风格一致
- 需要添加 `gorm.io/gorm` 导入
**验收标准**
- [x] 代码编译通过
- [ ] 输入不存在的邀请码时返回 "无邀请码" 提示
**预估复杂度**:低
---
### 任务3: 编译验证
**输入契约**
- 任务1和任务2已完成
**输出契约**
- 项目编译成功,无错误
**验收标准**
- [x] `go build ./...` 无报错
---
### 任务4: 更新说明文档
**输入契约**
- 任务3已完成
**输出契约**
- 更新 `说明文档.md` 记录本次修复
**验收标准**
- [x] 文档记录完整
---
## 执行顺序
1. ✅ 任务1 和 任务2 可并行执行(无依赖)
2. ✅ 任务3 在任务1、2完成后执行
3. ✅ 任务4 最后执行
-309
View File
@@ -1,309 +0,0 @@
第一个文件
server {
listen 80;
server_name airoport.org www.airoport.org api.airoport.win;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name airoport.org;
ssl_certificate /etc/letsencrypt/live/airoport.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/airoport.org/privkey.pem;
return 301 https://airoport.co$request_uri;
}
server {
listen 443 ssl http2;
server_name www.airoport.org;
ssl_certificate /etc/letsencrypt/live/www.airoport.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.airoport.org/privkey.pem;
return 301 https://www.airoport.co$request_uri;
}
server {
listen 443 ssl http2;
server_name api.airoport.win;
client_max_body_size 150M;
ssl_certificate /etc/letsencrypt/live/api.airoport.win/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.airoport.win/privkey.pem;
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_next_upstream timeout;
}
}
server {
listen 443 ssl http2;
server_name xqwbmzy8.de99e242.airoport.org;
client_max_body_size 150M;
ssl_certificate /etc/letsencrypt/live/xqwbmzy8.de99e242.airoport.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/xqwbmzy8.de99e242.airoport.org/privkey.pem;
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json image/svg+xml;
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
proxy_pass http://localhost8315:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
expires 1y;
add_header Cache-Control "public, immutable";
}
location ^~ / {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache;
proxy_ssl_server_name off;
proxy_ssl_name $proxy_host;
}
}
第二个文件:
server {
listen 80;
server_name airoport.co www.airoport.co api.airoport.co de99e242.airoport.co xqwbmzy8.de99e242.airoport.co api.airoport.win;
location / {
return 301 https://$host$request_uri;
}
}
# 主域名和www指向3002 (用户界面)
server {
listen 443 ssl http2;
server_name airoport.co;
client_max_body_size 150M;
ssl_certificate /etc/letsencrypt/live/airoport.co-0003/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/airoport.co-0003/privkey.pem;
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
location /md {
alias /var/www/md/;
# 启用目录浏览
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
# 设置默认文档为README.md
index README.md;
# 跨域处理
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials "true" always;
# 处理OPTIONS预检请求
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
add_header Content-Length 0;
add_header Content-Type text/plain;
return 204;
}
# 处理.md文件
location ~* \.md$ {
add_header Content-Type "text/markdown; charset=utf-8";
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
# 跨域处理
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
}
# 静态资源缓存优化
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options nosniff;
# 跨域处理
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
}
}
location ^~ / {
proxy_pass http://127.0.0.1:3002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache;
proxy_ssl_server_name off;
proxy_ssl_name $proxy_host;
}
}
server {
listen 443 ssl http2;
server_name www.airoport.co;
client_max_body_size 150M;
ssl_certificate /etc/letsencrypt/live/www.airoport.co/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.airoport.co/privkey.pem;
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
location ^~ / {
proxy_pass http://127.0.0.1:3002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache;
proxy_ssl_server_name off;
proxy_ssl_name $proxy_host;
}
}
server {
listen 443 ssl http2;
server_name api.airoport.co;
client_max_body_size 150M;
ssl_certificate /etc/letsencrypt/live/api.airoport.co/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.airoport.co/privkey.pem;
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 ssl http2;
server_name api.airoport.win;
client_max_body_size 150M;
ssl_certificate /etc/letsencrypt/live/api.airoport.win/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.airoport.win/privkey.pem;
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 10s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_next_upstream timeout;
}
}
# de99e242子域名指向3001 (管理界面)
server {
listen 443 ssl http2;
server_name xqwbmzy8.de99e242.airoport.co;
client_max_body_size 150M;
ssl_certificate /etc/letsencrypt/live/xqwbmzy8.de99e242.airoport.co/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/xqwbmzy8.de99e242.airoport.co/privkey.pem;
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json image/svg+xml;
location ^~ / {
proxy_pass http://127.0.0.1:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache;
proxy_ssl_server_name off;
proxy_ssl_name $proxy_host;
}
}
+45
View File
@@ -0,0 +1,45 @@
Host: # 服务监听地址
Port: 8080 # 服务监听端口, 默认: 8080
Debug: true # 是否开启调试模式, 默认: false
JwtAuth: # JWT认证配置
AccessSecret: CHANGE_ME_TO_A_RANDOM_SECRET # 访问令牌密钥, 请修改为随机字符串
AccessExpire: 604800 # 访问令牌过期时间,单位秒, 默认: 604800 (7天)
Logger: # 日志配置
FilePath: logs/ppanel.log # 日志文件路径
MaxSize: 50 # 日志文件最大大小, 单位MB
MaxBackup: 3 # 日志文件最大备份数
MaxAge: 30 # 日志文件最大保存时间,单位天
Compress: true # 是否压缩日志文件
Level: debug # 日志级别: debug, info, warn, error, panic, fatal
MySQL:
Addr: 127.0.0.1:3306 # MySQL地址
Username: root # MySQL用户名 (与创建的用户一致)
Password: password # MySQL密码 (换成之前生成的随机密码)
Dbname: ppanel # MySQL数据库名 (与脚本创建的数据库一致)
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
MaxIdleConns: 10
MaxOpenConns: 100
LogMode: debug
LogZap: true
SlowThreshold: 1000
Redis:
Host: 127.0.0.1:6379 # Redis地址,格式:host:port
Pass: # Redis密码,如果没有设置密码可以留空
DB: 0 # Redis数据库编号,默认0
PoolSize: 100 # 连接池大小(最大连接数),根据应用并发量调整,建议:小流量50-100,中流量100-300,大流量300-500
MinIdleConns: 10 # 最小空闲连接数,保持一定数量的空闲连接以减少建立连接的开销,建议为PoolSize的10%
MaxRetries: 3 # 最大重试次数,网络抖动时自动重试,建议2-3次
PoolTimeout: 4 # 连接池超时时间(秒),从连接池获取连接的最大等待时间,建议3-5秒
IdleTimeout: 300 # 空闲连接超时时间(秒),自动回收长时间空闲的连接,建议300-600秒(5-10分钟)
MaxConnAge: 0 # 连接最大生命周期(秒),定期重建连接避免长时间使用的问题,0表示不限制,建议7200秒(2小时)或0
DialTimeout: 5 # 连接超时时间(秒),建立新连接的超时时间,建议5秒
ReadTimeout: 3 # 读操作超时时间(秒),建议2-3秒
WriteTimeout: 3 # 写操作超时时间(秒),建议2-3秒
Administrator:
Email: admin@ppanel.dev # 后台登录邮箱,请修改
Password: CHANGE_ME_TO_STRONG_PASSWORD # 后台登录密码,请修改为强密码
+19 -18
View File
@@ -7,7 +7,7 @@ require (
github.com/alibabacloud-go/darabonba-openapi v0.1.18
github.com/alibabacloud-go/dysmsapi-20170525/v2 v2.0.18
github.com/alibabacloud-go/tea v1.2.2
github.com/alicebob/miniredis/v2 v2.35.0
github.com/alicebob/miniredis/v2 v2.34.0
github.com/anaskhan96/go-password-encoder v0.0.0-20201010210601-c765b799fd72
github.com/andybalholm/brotli v1.1.1
github.com/forgoer/openssl v1.6.0
@@ -17,7 +17,7 @@ require (
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.24.0
github.com/go-resty/resty/v2 v2.15.3
github.com/go-sql-driver/mysql v1.9.0
github.com/go-sql-driver/mysql v1.8.1
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/gofrs/uuid/v5 v5.3.0
github.com/golang-jwt/jwt/v5 v5.2.2
@@ -25,14 +25,14 @@ require (
github.com/gorilla/websocket v1.5.3
github.com/hibiken/asynq v0.24.1
github.com/jinzhu/copier v0.4.0
github.com/klauspost/compress v1.17.11
github.com/klauspost/compress v1.17.7
github.com/nyaruka/phonenumbers v1.5.0
github.com/pkg/errors v0.9.1
github.com/redis/go-redis/v9 v9.14.0
github.com/pkg/errors v0.9.1
github.com/redis/go-redis/v9 v9.7.2
github.com/smartwalle/alipay/v3 v3.2.23
github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.11.1
github.com/stretchr/testify v1.10.0
github.com/stripe/stripe-go/v81 v81.1.0
github.com/twilio/twilio-go v1.23.11
go.opentelemetry.io/otel v1.29.0
@@ -46,7 +46,7 @@ require (
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.35.0
golang.org/x/oauth2 v0.25.0
golang.org/x/time v0.10.0
golang.org/x/time v0.6.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
@@ -60,10 +60,10 @@ require (
github.com/fatih/color v1.18.0
github.com/goccy/go-json v0.10.4
github.com/golang-migrate/migrate/v4 v4.18.2
github.com/oschwald/geoip2-golang v1.13.0
github.com/spaolacci/murmur3 v1.1.0
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.36.5
gorm.io/driver/sqlite v1.4.4
google.golang.org/grpc v1.64.1
google.golang.org/protobuf v1.36.3
)
require (
@@ -79,11 +79,12 @@ require (
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect
github.com/aliyun/credentials-go v1.3.10 // indirect
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
github.com/bytedance/sonic v1.12.7 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.5.6 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
@@ -93,7 +94,7 @@ require (
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/glog v1.2.1 // indirect
github.com/golang/glog v1.2.0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
@@ -112,12 +113,12 @@ require (
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/openzipkin/zipkin-go v0.4.2 // indirect
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
@@ -134,15 +135,15 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.13.0 // indirect
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
+35 -32
View File
@@ -52,8 +52,10 @@ github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0=
github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.3.10 h1:45Xxrae/evfzQL9V10zL3xX31eqgLWEaIdCoPipOEQA=
@@ -76,8 +78,8 @@ github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOE
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -142,8 +144,8 @@ github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1
github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8=
github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
@@ -158,8 +160,8 @@ github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
@@ -230,8 +232,8 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
@@ -255,7 +257,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
@@ -282,8 +283,12 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA=
github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY=
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -293,8 +298,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/go-redis/v9 v9.7.2 h1:PSGhv13dJyrTCw1+55H0pIKM3WFov7HuUrKUmInGL0o=
github.com/redis/go-redis/v9 v9.7.2/go.mod h1:yp5+a5FnEEP0/zTYuw6u6/2nn3zivwhv274qYgWQhDM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
@@ -337,9 +342,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stripe/stripe-go/v81 v81.1.0 h1:OlpGPO2vhS2raLR/NuvHKeRUZ57FTkdZBTcd5Hhoyos=
github.com/stripe/stripe-go/v81 v81.1.0/go.mod h1:C/F4jlmnGNacvYtBp/LUHCvVUJEZffFQCobkzwY1WOo=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
@@ -383,8 +387,8 @@ go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt3
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@@ -431,8 +435,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
@@ -481,8 +485,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -503,16 +507,16 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No=
google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -521,8 +525,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -548,7 +552,6 @@ gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.23.0/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
+22 -1
View File
@@ -9,6 +9,7 @@ import (
"net/http"
"os"
"github.com/perfect-panel/server/internal/report"
"github.com/perfect-panel/server/pkg/logger"
"gorm.io/driver/mysql"
@@ -35,10 +36,30 @@ func Config(path string) (chan bool, *http.Server) {
configPath = path
// Create a new Gin instance
r := gin.Default()
// get server port
port := 8080
host := "127.0.0.1"
// check gateway mode
if report.IsGatewayMode() {
// get free port
freePort, err := report.ModulePort()
if err != nil {
logger.Errorf("get module port error: %s", err.Error())
panic(err)
}
port = freePort
// register module
err = report.RegisterModule(port)
if err != nil {
logger.Errorf("register module error: %s", err.Error())
panic(err)
}
logger.Infof("module registered on port %d", port)
}
// Create a new HTTP server
server := &http.Server{
Addr: ":8080",
Addr: fmt.Sprintf("%s:%d", host, port),
Handler: r,
}
// Load templates
+34
View File
@@ -0,0 +1,34 @@
package initialize
import (
"context"
"fmt"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/tool"
)
func Currency(ctx *svc.ServiceContext) {
// Retrieve system currency configuration
currency, err := ctx.SystemModel.GetCurrencyConfig(context.Background())
if err != nil {
logger.Errorf("[INIT] Failed to get currency configuration: %v", err.Error())
panic(fmt.Sprintf("[INIT] Failed to get currency configuration: %v", err.Error()))
}
// Parse currency configuration
configs := struct {
CurrencyUnit string
CurrencySymbol string
AccessKey string
}{}
tool.SystemConfigSliceReflectToStruct(currency, &configs)
ctx.ExchangeRate = 0 // Default exchange rate to 0
ctx.Config.Currency = config.Currency{
Unit: configs.CurrencyUnit,
Symbol: configs.CurrencySymbol,
AccessKey: configs.AccessKey,
}
logger.Infof("[INIT] Currency configuration: %v", ctx.Config.Currency)
}
+1
View File
@@ -15,6 +15,7 @@ func StartInitSystemConfig(svc *svc.ServiceContext) {
Subscribe(svc)
Register(svc)
Mobile(svc)
Currency(svc)
if !svc.Config.Debug {
Telegram(svc)
}
@@ -116,7 +116,7 @@ VALUES (1, 'site', 'SiteLogo', '/favicon.svg', 'string', 'Site Logo', '2025-04-2
'2025-04-22 14:25:16.641'),
(37, 'currency', 'AccessKey', '', 'string', 'Exchangerate Access Key', '2025-04-22 14:25:16.641',
'2025-04-22 14:25:16.641'),
(38, 'verify_code', 'VerifyCodeExpireTime', '900', 'int', 'Verify code expire time', '2025-04-22 14:25:16.641',
(38, 'verify_code', 'VerifyCodeExpireTime', '300', 'int', 'Verify code expire time', '2025-04-22 14:25:16.641',
'2025-04-22 14:25:16.641'),
(39, 'verify_code', 'VerifyCodeLimit', '15', 'int', 'limits of verify code', '2025-04-22 14:25:16.641',
'2025-04-22 14:25:16.641'),
@@ -1,2 +1,2 @@
ALTER TABLE traffic_log ADD INDEX IF NOT EXISTS idx_timestamp (timestamp);
CREATE INDEX idx_timestamp ON traffic_log (timestamp);
+31 -20
View File
@@ -1,4 +1,6 @@
-- Add longitude if not exists
-- Only add the columns to `servers` when they do not already exist
-- Add longitude
SET @col_exists := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
@@ -6,16 +8,18 @@ SET @col_exists := (
AND TABLE_NAME = 'servers'
AND COLUMN_NAME = 'longitude'
);
SET @sql := IF(
SET @query := IF(
@col_exists = 0,
'ALTER TABLE `servers` ADD COLUMN `longitude` VARCHAR(255) DEFAULT '''' COMMENT ''longitude''',
'SELECT 1'
'ALTER TABLE `servers` ADD COLUMN `longitude` VARCHAR(255) DEFAULT '''' COMMENT ''longitude'';',
'SELECT "Column `longitude` already exists"'
);
PREPARE stmt FROM @sql;
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Add latitude if not exists
-- Add latitude
SET @col_exists := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
@@ -23,16 +27,18 @@ SET @col_exists := (
AND TABLE_NAME = 'servers'
AND COLUMN_NAME = 'latitude'
);
SET @sql := IF(
SET @query := IF(
@col_exists = 0,
'ALTER TABLE `servers` ADD COLUMN `latitude` VARCHAR(255) DEFAULT '''' COMMENT ''latitude''',
'SELECT 1'
'ALTER TABLE `servers` ADD COLUMN `latitude` VARCHAR(255) DEFAULT '''' COMMENT ''latitude'';',
'SELECT "Column `latitude` already exists"'
);
PREPARE stmt FROM @sql;
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Add longitude_center if not exists
-- Add longitude_center
SET @col_exists := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
@@ -40,16 +46,18 @@ SET @col_exists := (
AND TABLE_NAME = 'servers'
AND COLUMN_NAME = 'longitude_center'
);
SET @sql := IF(
SET @query := IF(
@col_exists = 0,
'ALTER TABLE `servers` ADD COLUMN `longitude_center` VARCHAR(255) DEFAULT '''' COMMENT ''longitude center''',
'SELECT 1'
'ALTER TABLE `servers` ADD COLUMN `longitude_center` VARCHAR(255) DEFAULT '''' COMMENT ''longitude center'';',
'SELECT "Column `longitude_center` already exists"'
);
PREPARE stmt FROM @sql;
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Add latitude_center if not exists
-- Add latitude_center
SET @col_exists := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
@@ -57,11 +65,14 @@ SET @col_exists := (
AND TABLE_NAME = 'servers'
AND COLUMN_NAME = 'latitude_center'
);
SET @sql := IF(
SET @query := IF(
@col_exists = 0,
'ALTER TABLE `servers` ADD COLUMN `latitude_center` VARCHAR(255) DEFAULT '''' COMMENT ''latitude center''',
'SELECT 1'
'ALTER TABLE `servers` ADD COLUMN `latitude_center` VARCHAR(255) DEFAULT '''' COMMENT ''latitude center'';',
'SELECT "Column `latitude_center` already exists"'
);
PREPARE stmt FROM @sql;
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
@@ -1 +0,0 @@
DROP TABLE IF EXISTS `log_message`;
@@ -1,27 +0,0 @@
CREATE TABLE `log_message` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`platform` VARCHAR(32) NOT NULL,
`app_version` VARCHAR(32) NULL,
`os_name` VARCHAR(32) NULL,
`os_version` VARCHAR(32) NULL,
`device_id` VARCHAR(64) NULL,
`user_id` BIGINT NULL DEFAULT NULL,
`session_id` VARCHAR(64) NULL,
`level` TINYINT UNSIGNED NOT NULL DEFAULT 3,
`error_code` VARCHAR(64) NULL,
`message` TEXT NOT NULL,
`stack` MEDIUMTEXT NULL,
`context` JSON NULL,
`client_ip` VARCHAR(45) NULL,
`user_agent` VARCHAR(255) NULL,
`locale` VARCHAR(16) NULL,
`digest` VARCHAR(64) NULL,
`occurred_at` DATETIME NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_digest` (`digest`),
KEY `idx_platform_time` (`platform`, `created_at`),
KEY `idx_user_time` (`user_id`, `created_at`),
KEY `idx_device_time` (`device_id`, `created_at`),
KEY `idx_error_code` (`error_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -0,0 +1,2 @@
ALTER TABLE `user_subscribe`
DROP COLUMN `note`;
@@ -0,0 +1,4 @@
ALTER TABLE `user_subscribe`
ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT ''
COMMENT 'User note for subscription'
AFTER `status`;
@@ -1 +0,0 @@
DROP TABLE IF EXISTS `apple_iap_transactions`;
@@ -1,15 +0,0 @@
CREATE TABLE IF NOT EXISTS `apple_iap_transactions` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT 'User ID',
`original_transaction_id` varchar(255) NOT NULL COMMENT 'Original Transaction ID',
`transaction_id` varchar(255) NOT NULL COMMENT 'Transaction ID',
`product_id` varchar(255) NOT NULL COMMENT 'Product ID',
`purchase_at` datetime NOT NULL COMMENT 'Purchase Time',
`revocation_at` datetime DEFAULT NULL COMMENT 'Revocation Time',
`jws_hash` varchar(255) NOT NULL COMMENT 'JWS Hash',
`created_at` datetime DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_original` (`original_transaction_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
@@ -0,0 +1,2 @@
ALTER TABLE `user`
DROP COLUMN IF EXISTS `rules`;
@@ -0,0 +1,4 @@
ALTER TABLE `user`
ADD COLUMN `rules` TEXT NULL
COMMENT 'User rules for subscription'
AFTER `created_at`;
@@ -1 +0,0 @@
ALTER TABLE user DROP COLUMN last_login_time;
@@ -1 +0,0 @@
ALTER TABLE user ADD COLUMN last_login_time DATETIME DEFAULT NULL COMMENT 'Last Login Time';
@@ -0,0 +1,5 @@
DROP TABLE IF EXISTS `withdrawals`;
DELETE FROM `system`
WHERE `category` = 'invite'
AND `key` = 'WithdrawalMethod';
@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS `withdrawals` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
`user_id` BIGINT NOT NULL COMMENT 'User ID',
`amount` BIGINT NOT NULL COMMENT 'Withdrawal Amount',
`content` TEXT COMMENT 'Withdrawal Content',
`status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Withdrawal Status',
`reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Rejection Reason',
`created_at` DATETIME NOT NULL COMMENT 'Creation Time',
`updated_at` DATETIME NOT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
VALUES
('invite', 'WithdrawalMethod', '', 'string', 'withdrawal method', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637');
@@ -0,0 +1,2 @@
ALTER TABLE `subscribe`
DROP COLUMN `show_original_price`;
@@ -0,0 +1,2 @@
ALTER TABLE `subscribe`
ADD COLUMN `show_original_price` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'display the original price: 0 not display, 1 display' AFTER `created_at`;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS `server_group`;
@@ -0,0 +1,5 @@
-- This migration script reverts the inventory values in the 'subscribe' table
UPDATE `subscribe`
SET `inventory` = 0
WHERE `inventory` = -1;
@@ -0,0 +1,4 @@
-- Update the `subscribe` table to set `inventory` to -1 where it is currently 0
UPDATE `subscribe`
SET `inventory` = -1
WHERE `inventory` = 0;
@@ -0,0 +1,5 @@
-- Drop redemption_record table
DROP TABLE IF EXISTS `redemption_record`;
-- Drop redemption_code table
DROP TABLE IF EXISTS `redemption_code`;
@@ -0,0 +1,31 @@
-- Create redemption_code table
CREATE TABLE IF NOT EXISTS `redemption_code` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
`code` VARCHAR(255) NOT NULL COMMENT 'Redemption Code',
`total_count` BIGINT NOT NULL DEFAULT 0 COMMENT 'Total Redemption Count',
`used_count` BIGINT NOT NULL DEFAULT 0 COMMENT 'Used Redemption Count',
`subscribe_plan` BIGINT NOT NULL DEFAULT 0 COMMENT 'Subscribe Plan',
`unit_time` VARCHAR(50) NOT NULL DEFAULT 'month' COMMENT 'Unit Time: day, month, quarter, half_year, year',
`quantity` BIGINT NOT NULL DEFAULT 1 COMMENT 'Quantity',
`created_at` DATETIME NOT NULL COMMENT 'Creation Time',
`updated_at` DATETIME NOT NULL COMMENT 'Update Time',
`deleted_at` DATETIME DEFAULT NULL COMMENT 'Deletion Time',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Redemption Code Table';
-- Create redemption_record table
CREATE TABLE IF NOT EXISTS `redemption_record` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
`redemption_code_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'Redemption Code Id',
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'User Id',
`subscribe_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'Subscribe Id',
`unit_time` VARCHAR(50) NOT NULL DEFAULT 'month' COMMENT 'Unit Time',
`quantity` BIGINT NOT NULL DEFAULT 1 COMMENT 'Quantity',
`redeemed_at` DATETIME NOT NULL COMMENT 'Redeemed Time',
`created_at` DATETIME NOT NULL COMMENT 'Creation Time',
PRIMARY KEY (`id`),
KEY `idx_redemption_code_id` (`redemption_code_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Redemption Record Table';
@@ -0,0 +1,2 @@
-- Remove status column from redemption_code table
ALTER TABLE `redemption_code` DROP COLUMN `status`;
@@ -0,0 +1,2 @@
-- Add status column to redemption_code table
ALTER TABLE `redemption_code` ADD COLUMN `status` TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1=enabled, 0=disabled' AFTER `quantity`;
@@ -0,0 +1,2 @@
-- Remove device limit configuration from system table
DELETE FROM `system` WHERE `category` = 'register' AND `key` = 'DeviceLimit';
@@ -0,0 +1,3 @@
-- Add device limit configuration to system table
INSERT IGNORE INTO `system` (`id`, `category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
VALUES (43, 'register', 'DeviceLimit', '5', 'int', 'Device binding limit', NOW(), NOW());
@@ -0,0 +1,2 @@
-- Remove short_code column from user_device table
ALTER TABLE `user_device` DROP COLUMN `short_code`;
@@ -0,0 +1,2 @@
-- Add short_code column to user_device table
ALTER TABLE `user_device` ADD COLUMN `short_code` VARCHAR(255) DEFAULT '' COMMENT 'Short Code' AFTER `identifier`;
@@ -0,0 +1,2 @@
-- Remove index on refer_code column
ALTER TABLE `user` DROP INDEX `idx_refer_code`;
@@ -0,0 +1,2 @@
-- Add index on refer_code column for faster lookup
ALTER TABLE `user` ADD INDEX `idx_refer_code` (`refer_code`);
+20 -41
View File
@@ -7,6 +7,7 @@ import (
"github.com/perfect-panel/server/pkg/logger"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/logic/telegram"
"github.com/perfect-panel/server/internal/model/auth"
"github.com/perfect-panel/server/internal/svc"
@@ -14,51 +15,33 @@ import (
)
func Telegram(svc *svc.ServiceContext) {
logger.Infof("Telegram Config Enable: %v", svc.Config.Telegram.Enable)
if !svc.Config.Telegram.Enable {
logger.Info("Telegram disabled, skipping initialization")
method, err := svc.AuthModel.FindOneByMethod(context.Background(), "telegram")
if err != nil {
logger.Errorf("[Init Telegram Config] Get Telegram Config Error: %s", err.Error())
return
}
var tg config.Telegram
tgConfig := new(auth.TelegramAuthConfig)
if err = tgConfig.Unmarshal(method.Config); err != nil {
logger.Errorf("[Init Telegram Config] Unmarshal Telegram Config Error: %s", err.Error())
return
}
// Prefer BotToken from DB auth method, fallback to config file
var usedToken string
var webHookDomain string
method, err := svc.AuthModel.FindOneByMethod(context.Background(), "telegram")
if err == nil {
tgConfig := new(auth.TelegramAuthConfig)
if err = tgConfig.Unmarshal(method.Config); err == nil {
usedToken = tgConfig.BotToken
webHookDomain = tgConfig.WebHookDomain
} else {
logger.Errorf("[Init Telegram Config] Unmarshal Telegram Config Error: %s", err.Error())
}
} else {
logger.Debugf("[Init Telegram Config] No Telegram method in DB, fallback to file config: %s", err.Error())
}
if usedToken == "" {
usedToken = svc.Config.Telegram.BotToken
}
if webHookDomain == "" {
webHookDomain = svc.Config.Telegram.WebHookDomain
}
if usedToken == "" {
if tgConfig.BotToken == "" {
logger.Debug("[Init Telegram Config] Telegram Token is empty")
return
}
logger.Info("Initializing Telegram Bot API...")
bot, err := tgbotapi.NewBotAPI(usedToken)
bot, err := tgbotapi.NewBotAPI(tg.BotToken)
if err != nil {
logger.Error("[Init Telegram Config] New Bot API Error: ", logger.Field("error", err.Error()))
return
}
if webHookDomain == "" || svc.Config.Debug {
// Ensure webhook is removed to avoid long polling conflict
if _, derr := bot.MakeRequest("deleteWebhook", tgbotapi.Params{}); derr != nil {
logger.Errorf("[Init Telegram Config] Delete webhook failed: %s", derr.Error())
}
// Long Polling mode
if tgConfig.WebHookDomain == "" || svc.Config.Debug {
// set Long Polling mode
updateConfig := tgbotapi.NewUpdate(0)
updateConfig.Timeout = 60
updates := bot.GetUpdatesChan(updateConfig)
@@ -72,7 +55,7 @@ func Telegram(svc *svc.ServiceContext) {
}
}()
} else {
wh, err := tgbotapi.NewWebhook(fmt.Sprintf("%s/v1/telegram/webhook?secret=%s", webHookDomain, tool.Md5Encode(usedToken, false)))
wh, err := tgbotapi.NewWebhook(fmt.Sprintf("%s/v1/telegram/webhook?secret=%s", tgConfig.WebHookDomain, tool.Md5Encode(tgConfig.BotToken, false)))
if err != nil {
logger.Errorf("[Init Telegram Config] New Webhook Error: %s", err.Error())
return
@@ -91,13 +74,9 @@ func Telegram(svc *svc.ServiceContext) {
}
svc.Config.Telegram.BotID = user.ID
svc.Config.Telegram.BotName = user.UserName
svc.Config.Telegram.BotToken = usedToken
svc.Config.Telegram.WebHookDomain = webHookDomain
svc.Config.Telegram.EnableNotify = tg.EnableNotify
svc.Config.Telegram.WebHookDomain = tg.WebHookDomain
svc.TelegramBot = bot
if webHookDomain == "" || svc.Config.Debug {
logger.Info("[Init Telegram Config] Long polling mode initialized")
} else {
logger.Info("[Init Telegram Config] Webhook set success")
}
logger.Info("[Init Telegram Config] Webhook set success")
}
+2 -3
View File
@@ -42,9 +42,6 @@ const SessionIdKey = "auth:session_id"
// DeviceCacheKeyKey cache session key
const DeviceCacheKeyKey = "auth:device_identifier"
// UserSessionsKeyPrefix per-user sessions zset key prefix
const UserSessionsKeyPrefix = "auth:user_sessions:"
// GlobalConfigKey Global Config Key
const GlobalConfigKey = "system:global_config"
@@ -65,3 +62,5 @@ const SendIntervalKeyPrefix = "send:interval:"
// SendCountLimitKeyPrefix Send Count Limit Key Prefix eg. send:limit:register:email:xxx@ppanel.dev
const SendCountLimitKeyPrefix = "send:limit:"
const RegisterIpKeyPrefix = "register:ip:"
+23 -11
View File
@@ -5,7 +5,6 @@ import (
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/orm"
"github.com/perfect-panel/server/pkg/trace"
)
type Config struct {
@@ -30,7 +29,7 @@ type Config struct {
Invite InviteConfig `yaml:"Invite"`
Telegram Telegram `yaml:"Telegram"`
Log Log `yaml:"Log"`
Trace trace.Config `yaml:"Trace"`
Currency Currency `yaml:"Currency"`
Administrator struct {
Email string `yaml:"Email" default:"admin@ppanel.dev"`
Password string `yaml:"Password" default:"password"`
@@ -38,15 +37,23 @@ type Config struct {
}
type RedisConfig struct {
Host string `yaml:"Host" default:"localhost:6379"`
Pass string `yaml:"Pass" default:""`
DB int `yaml:"DB" default:"0"`
Host string `yaml:"Host" default:"localhost:6379"`
Pass string `yaml:"Pass" default:""`
DB int `yaml:"DB" default:"0"`
PoolSize int `yaml:"PoolSize" default:"100"` // 连接池大小(最大连接数)
MinIdleConns int `yaml:"MinIdleConns" default:"10"` // 最小空闲连接数
MaxRetries int `yaml:"MaxRetries" default:"3"` // 最大重试次数
PoolTimeout int `yaml:"PoolTimeout" default:"4"` // 连接池超时时间(秒)
IdleTimeout int `yaml:"IdleTimeout" default:"300"` // 空闲连接超时时间(秒)
MaxConnAge int `yaml:"MaxConnAge" default:"0"` // 连接最大生命周期(秒),0表示不限制
DialTimeout int `yaml:"DialTimeout" default:"5"` // 连接超时时间(秒)
ReadTimeout int `yaml:"ReadTimeout" default:"3"` // 读超时时间(秒)
WriteTimeout int `yaml:"WriteTimeout" default:"3"` // 写超时时间(秒)
}
type JwtAuth struct {
AccessSecret string `yaml:"AccessSecret"`
AccessExpire int64 `yaml:"AccessExpire" default:"604800"`
MaxSessionsPerUser int64 `yaml:"MaxSessionsPerUser" default:"1"`
AccessSecret string `yaml:"AccessSecret"`
AccessExpire int64 `yaml:"AccessExpire" default:"604800"`
}
type Verify struct {
@@ -75,6 +82,7 @@ type RegisterConfig struct {
IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"`
IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"`
EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"`
DeviceLimit int64 `yaml:"DeviceLimit" default:"5"`
}
type EmailConfig struct {
@@ -206,7 +214,6 @@ type InviteConfig struct {
ForcedInvite bool `yaml:"ForcedInvite" default:"false"`
ReferralPercentage int64 `yaml:"ReferralPercentage" default:"0"`
OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"`
GiftDays int64 `yaml:"GiftDays" default:"0"`
}
type Telegram struct {
@@ -214,7 +221,6 @@ type Telegram struct {
BotID int64 `yaml:"BotID" default:""`
BotName string `yaml:"BotName" default:""`
BotToken string `yaml:"BotToken" default:""`
GroupChatID string `yaml:"GroupChatID" default:""`
EnableNotify bool `yaml:"EnableNotify" default:"false"`
WebHookDomain string `yaml:"WebHookDomain" default:""`
}
@@ -226,7 +232,7 @@ type TLS struct {
}
type VerifyCode struct {
ExpireTime int64 `yaml:"ExpireTime" default:"900"`
ExpireTime int64 `yaml:"ExpireTime" default:"300"`
Limit int64 `yaml:"Limit" default:"15"`
Interval int64 `yaml:"Interval" default:"60"`
}
@@ -246,3 +252,9 @@ type NodeDBConfig struct {
Block string
Outbound string
}
type Currency struct {
Unit string `yaml:"Unit" default:"CNY"`
Symbol string `yaml:"Symbol" default:"USD"`
AccessKey string `yaml:"AccessKey" default:""`
}
@@ -1,17 +0,0 @@
package log
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/log"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/result"
)
func GetErrorLogMessageDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
id := c.Query("id")
l := log.NewGetErrorLogMessageDetailLogic(c.Request.Context(), svcCtx)
resp, err := l.GetErrorLogMessageDetail(id)
result.HttpResult(c, resp, err)
}
}
@@ -1,19 +0,0 @@
package log
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/log"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
func GetErrorLogMessageListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.GetErrorLogMessageListRequest
_ = c.ShouldBind(&req)
l := log.NewGetErrorLogMessageListLogic(c.Request.Context(), svcCtx)
resp, err := l.GetErrorLogMessageList(&req)
result.HttpResult(c, resp, err)
}
}
@@ -0,0 +1,26 @@
package redemption
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/redemption"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Batch delete redemption code
func BatchDeleteRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.BatchDeleteRedemptionCodeRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := redemption.NewBatchDeleteRedemptionCodeLogic(c.Request.Context(), svcCtx)
err := l.BatchDeleteRedemptionCode(&req)
result.HttpResult(c, nil, err)
}
}

Some files were not shown because too many files have changed in this diff Show More